/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.telephony; import static android.telephony.PhoneCapability.DEVICE_NR_CAPABILITY_NSA; import static android.telephony.PhoneCapability.DEVICE_NR_CAPABILITY_SA; import static com.android.internal.telephony.RILConstants.RADIO_NOT_AVAILABLE; import static com.android.internal.telephony.RILConstants.REQUEST_NOT_SUPPORTED; import static com.android.internal.telephony.RILConstants.RIL_REQUEST_GET_HAL_DEVICE_CAPABILITIES; import static com.android.internal.telephony.RILConstants.RIL_REQUEST_GET_PHONE_CAPABILITY; import static com.android.internal.telephony.RILConstants.RIL_REQUEST_GET_SIMULTANEOUS_CALLING_SUPPORT; import static com.android.internal.telephony.RILConstants.RIL_REQUEST_GET_SLOT_STATUS; import static com.android.internal.telephony.RILConstants.RIL_REQUEST_SET_LOGICAL_TO_PHYSICAL_SLOT_MAPPING; import static com.android.internal.telephony.RILConstants.RIL_REQUEST_SET_PREFERRED_DATA_MODEM; import static com.android.internal.telephony.RILConstants.RIL_REQUEST_SWITCH_DUAL_SIM_CONFIG; import android.content.Context; import android.os.AsyncResult; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Registrant; import android.os.RemoteException; import android.os.ServiceManager; import android.os.Trace; import android.os.WorkSource; import android.telephony.TelephonyManager; import android.telephony.UiccSlotMapping; import android.util.SparseArray; import com.android.telephony.Rlog; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicLong; /** * This class provides wrapper APIs for IRadioConfig interface. */ public class RadioConfig extends Handler { private static final String TAG = "RadioConfig"; private static final boolean DBG = true; private static final boolean VDBG = false; //STOPSHIP if true private static final Object sLock = new Object(); static final int EVENT_HIDL_SERVICE_DEAD = 1; static final int EVENT_AIDL_SERVICE_DEAD = 2; private final boolean mIsMobileNetworkSupported; private final SparseArray mRequestList = new SparseArray<>(); /* default work source which will blame phone process */ private final WorkSource mDefaultWorkSource; private final int[] mDeviceNrCapabilities; private final AtomicLong mRadioConfigProxyCookie = new AtomicLong(0); private final RadioConfigProxy mRadioConfigProxy; private MockModem mMockModem; private static Context sContext; private static RadioConfig sRadioConfig; protected Registrant mSimSlotStatusRegistrant; protected Registrant mSimultaneousCallingSupportStatusRegistrant; private boolean isMobileDataCapable(Context context) { final TelephonyManager tm = context.getSystemService(TelephonyManager.class); return tm != null && tm.isDataCapable(); } private RadioConfig(Context context, HalVersion radioHalVersion) { mIsMobileNetworkSupported = isMobileDataCapable(context); mRadioConfigProxy = new RadioConfigProxy(this, radioHalVersion); mDefaultWorkSource = new WorkSource(context.getApplicationInfo().uid, context.getPackageName()); boolean is5gStandalone = context.getResources().getBoolean( com.android.internal.R.bool.config_telephony5gStandalone); boolean is5gNonStandalone = context.getResources().getBoolean( com.android.internal.R.bool.config_telephony5gNonStandalone); if (!is5gStandalone && !is5gNonStandalone) { mDeviceNrCapabilities = new int[0]; } else { List list = new ArrayList<>(); if (is5gNonStandalone) { list.add(DEVICE_NR_CAPABILITY_NSA); } if (is5gStandalone) { list.add(DEVICE_NR_CAPABILITY_SA); } mDeviceNrCapabilities = list.stream().mapToInt(Integer::valueOf).toArray(); } } /** * Returns the singleton static instance of RadioConfig */ public static RadioConfig getInstance() { synchronized (sLock) { if (sRadioConfig == null) { throw new RuntimeException( "RadioConfig.getInstance can't be called before make()"); } return sRadioConfig; } } /** * Makes the radio config based on the context and the radio hal version passed in */ public static RadioConfig make(Context c, HalVersion radioHalVersion) { synchronized (sLock) { if (sRadioConfig != null) { throw new RuntimeException("RadioConfig.make() should only be called once"); } sContext = c; sRadioConfig = new RadioConfig(c, radioHalVersion); return sRadioConfig; } } @Override public void handleMessage(Message message) { if (message.what == EVENT_HIDL_SERVICE_DEAD) { logd("handleMessage: EVENT_HIDL_SERVICE_DEAD cookie = " + message.obj + " mRadioConfigProxyCookie = " + mRadioConfigProxyCookie.get()); if ((long) message.obj == mRadioConfigProxyCookie.get()) { resetProxyAndRequestList("EVENT_HIDL_SERVICE_DEAD", null); } } else if (message.what == EVENT_AIDL_SERVICE_DEAD) { logd("handleMessage: EVENT_AIDL_SERVICE_DEAD mRadioConfigProxyCookie = " + mRadioConfigProxyCookie.get()); resetProxyAndRequestList("EVENT_AIDL_SERVICE_DEAD", null); } } /** * Release each request in mRequestList then clear the list * @param error is the RIL_Errno sent back * @param loggable true means to print all requests in mRequestList */ private void clearRequestList(int error, boolean loggable) { RILRequest rr; synchronized (mRequestList) { int count = mRequestList.size(); if (DBG && loggable) { logd("clearRequestList: mRequestList=" + count); } for (int i = 0; i < count; i++) { rr = mRequestList.valueAt(i); if (DBG && loggable) { logd(i + ": [" + rr.mSerial + "] " + RILUtils.requestToString(rr.mRequest)); } rr.onError(error, null); rr.release(); } mRequestList.clear(); } } private void resetProxyAndRequestList(String caller, Exception e) { loge(caller + ": " + e); mRadioConfigProxy.clear(); // increment the cookie so that death notification can be ignored mRadioConfigProxyCookie.incrementAndGet(); RILRequest.resetSerial(); // Clear request list on close clearRequestList(RADIO_NOT_AVAILABLE, false); getRadioConfigProxy(null); } /** * Returns a holder that has either: * - getV1() -> {@link android.hardware.radio.config.V1_0.IRadioConfig} * - getV2() -> {@link android.hardware.radio.config.IRadioConfig} * that returns corresponding hal implementation */ public RadioConfigProxy getRadioConfigProxy(Message result) { if (!mIsMobileNetworkSupported) { if (VDBG) logd("getRadioConfigProxy: Not calling getService(): wifi-only"); if (result != null) { AsyncResult.forMessage(result, null, CommandException.fromRilErrno(RADIO_NOT_AVAILABLE)); result.sendToTarget(); } mRadioConfigProxy.clear(); return mRadioConfigProxy; } if (!mRadioConfigProxy.isEmpty()) { return mRadioConfigProxy; } updateRadioConfigProxy(); if (mRadioConfigProxy.isEmpty() && result != null) { AsyncResult.forMessage( result, null, CommandException.fromRilErrno(RADIO_NOT_AVAILABLE)); result.sendToTarget(); } return mRadioConfigProxy; } /** * Request to enable/disable the mock modem service. * This is invoked from shell commands during CTS testing only. * * @param serviceName the service name we want to bind to */ public boolean setModemService(String serviceName) { boolean serviceBound = true; if (serviceName != null) { logd("Overriding connected service to MockModemService"); mMockModem = null; mMockModem = new MockModem(sContext, serviceName); if (mMockModem == null) { loge("MockModem creation failed."); return false; } mMockModem.bindToMockModemService(MockModem.RADIOCONFIG_SERVICE); int retryCount = 0; IBinder binder; do { binder = mMockModem.getServiceBinder(MockModem.RADIOCONFIG_SERVICE); retryCount++; if (binder == null) { logd("Retry(" + retryCount + ") Mock RadioConfig"); try { Thread.sleep(MockModem.BINDER_RETRY_MILLIS); } catch (InterruptedException e) { } } } while ((binder == null) && (retryCount < MockModem.BINDER_MAX_RETRY)); if (binder == null) { loge("Mock RadioConfig bind fail"); serviceBound = false; } if (serviceBound) resetProxyAndRequestList("EVENT_HIDL_SERVICE_DEAD", null); } if ((serviceName == null) || (!serviceBound)) { if (serviceBound) logd("Unbinding to mock RadioConfig service"); if (mMockModem != null) { mMockModem = null; resetProxyAndRequestList("EVENT_AIDL_SERVICE_DEAD", null); } } return serviceBound; } private void updateRadioConfigProxy() { IBinder service; if (mMockModem == null) { service = ServiceManager.waitForDeclaredService( android.hardware.radio.config.IRadioConfig.DESCRIPTOR + "/default"); } else { // Binds to Mock RadioConfig Service service = mMockModem.getServiceBinder(MockModem.RADIOCONFIG_SERVICE); } if (service != null) { mRadioConfigProxy.setAidl( android.hardware.radio.config.IRadioConfig.Stub.asInterface(service)); } if (mRadioConfigProxy.isEmpty()) { try { mRadioConfigProxy.setHidl(RIL.RADIO_HAL_VERSION_1_3, android.hardware.radio.config.V1_3.IRadioConfig.getService(true)); } catch (RemoteException | NoSuchElementException e) { mRadioConfigProxy.clear(); loge("getHidlRadioConfigProxy1_3: RadioConfigProxy getService: " + e); } } if (mRadioConfigProxy.isEmpty()) { try { mRadioConfigProxy.setHidl(RIL.RADIO_HAL_VERSION_1_1, android.hardware.radio.config.V1_1.IRadioConfig.getService(true)); } catch (RemoteException | NoSuchElementException e) { mRadioConfigProxy.clear(); loge("getHidlRadioConfigProxy1_1: RadioConfigProxy getService | linkToDeath: " + e); } } if (mRadioConfigProxy.isEmpty()) { loge("IRadioConfig <1.1 is no longer supported."); } if (!mRadioConfigProxy.isEmpty()) { try { mRadioConfigProxy.linkToDeath(mRadioConfigProxyCookie.incrementAndGet()); mRadioConfigProxy.setResponseFunctions(this); return; } catch (RemoteException e) { mRadioConfigProxy.clear(); loge("RadioConfigProxy: failed to linkToDeath() or setResponseFunction()"); } } loge("getRadioConfigProxy: mRadioConfigProxy == null"); } private RILRequest obtainRequest(int request, Message result, WorkSource workSource) { RILRequest rr = RILRequest.obtain(request, result, workSource); Trace.asyncTraceForTrackBegin( Trace.TRACE_TAG_NETWORK, "RIL", RILUtils.requestToString(rr.mRequest), rr.mSerial); synchronized (mRequestList) { mRequestList.append(rr.mSerial, rr); } return rr; } private RILRequest findAndRemoveRequestFromList(int serial) { RILRequest rr; synchronized (mRequestList) { rr = mRequestList.get(serial); if (rr != null) { Trace.asyncTraceForTrackEnd( Trace.TRACE_TAG_NETWORK, "RIL", rr.mSerial); mRequestList.remove(serial); } } return rr; } /** * This is a helper function to be called when a RadioConfigResponse callback is called. * It finds and returns RILRequest corresponding to the response if one is found. * @param responseInfo RadioResponseInfo received in response callback * @return RILRequest corresponding to the response */ public RILRequest processResponse(android.hardware.radio.RadioResponseInfo responseInfo) { int serial = responseInfo.serial; int error = responseInfo.error; int type = responseInfo.type; if (type != android.hardware.radio.RadioResponseType.SOLICITED) { loge("processResponse: Unexpected response type " + type); } RILRequest rr = findAndRemoveRequestFromList(serial); if (rr == null) { loge("processResponse: Unexpected response! serial: " + serial + " error: " + error); return null; } return rr; } /** * This is a helper function to be called when a RadioConfigResponse callback is called. * It finds and returns RILRequest corresponding to the response if one is found. * @param responseInfo RadioResponseInfo received in response callback * @return RILRequest corresponding to the response */ public RILRequest processResponse(android.hardware.radio.V1_0.RadioResponseInfo responseInfo) { int serial = responseInfo.serial; int error = responseInfo.error; int type = responseInfo.type; if (type != android.hardware.radio.RadioResponseType.SOLICITED) { loge("processResponse: Unexpected response type " + type); } RILRequest rr = findAndRemoveRequestFromList(serial); if (rr == null) { loge("processResponse: Unexpected response! serial: " + serial + " error: " + error); return null; } return rr; } /** * This is a helper function to be called when a RadioConfigResponse callback is called. * It finds and returns RILRequest corresponding to the response if one is found. * @param responseInfo RadioResponseInfo received in response callback * @return RILRequest corresponding to the response */ public RILRequest processResponse_1_6( android.hardware.radio.V1_6.RadioResponseInfo responseInfo) { int serial = responseInfo.serial; int error = responseInfo.error; int type = responseInfo.type; if (type != android.hardware.radio.RadioResponseType.SOLICITED) { loge("processResponse: Unexpected response type " + type); } RILRequest rr = findAndRemoveRequestFromList(serial); if (rr == null) { loge("processResponse: Unexpected response! serial: " + serial + " error: " + error); return null; } return rr; } /** * Wrapper function for IRadioConfig.getSimSlotsStatus(). */ public void getSimSlotsStatus(Message result) { RadioConfigProxy proxy = getRadioConfigProxy(result); if (proxy.isEmpty()) return; RILRequest rr = obtainRequest(RIL_REQUEST_GET_SLOT_STATUS, result, mDefaultWorkSource); if (DBG) { logd(rr.serialString() + "> " + RILUtils.requestToString(rr.mRequest)); } try { proxy.getSimSlotStatus(rr.mSerial); } catch (RemoteException | RuntimeException e) { resetProxyAndRequestList("getSimSlotsStatus", e); } } /** * Wrapper function for IRadioConfig.setPreferredDataModem(int modemId). */ public void setPreferredDataModem(int modemId, Message result) { RadioConfigProxy proxy = getRadioConfigProxy(null); if (proxy.isEmpty()) return; if (!isSetPreferredDataCommandSupported()) { if (result != null) { AsyncResult.forMessage(result, null, CommandException.fromRilErrno(REQUEST_NOT_SUPPORTED)); result.sendToTarget(); } return; } RILRequest rr = obtainRequest(RIL_REQUEST_SET_PREFERRED_DATA_MODEM, result, mDefaultWorkSource); if (DBG) { logd(rr.serialString() + "> " + RILUtils.requestToString(rr.mRequest)); } try { proxy.setPreferredDataModem(rr.mSerial, modemId); } catch (RemoteException | RuntimeException e) { resetProxyAndRequestList("setPreferredDataModem", e); } } /** * Wrapper function for IRadioConfig.getSimultaneousCallingSupport(). */ public void updateSimultaneousCallingSupport(Message result) { RadioConfigProxy proxy = getRadioConfigProxy(null); if (proxy.isEmpty()) return; if (proxy.getVersion().less(RIL.RADIO_HAL_VERSION_2_2)) { if (result != null) { AsyncResult.forMessage(result, null, CommandException.fromRilErrno(REQUEST_NOT_SUPPORTED)); result.sendToTarget(); } return; } RILRequest rr = obtainRequest(RIL_REQUEST_GET_SIMULTANEOUS_CALLING_SUPPORT, result, mDefaultWorkSource); if (DBG) { logd(rr.serialString() + "> " + RILUtils.requestToString(rr.mRequest)); } try { proxy.updateSimultaneousCallingSupport(rr.mSerial); } catch (RemoteException | RuntimeException e) { resetProxyAndRequestList("updateSimultaneousCallingSupport", e); } } /** * Wrapper function for IRadioConfig.getPhoneCapability(). */ public void getPhoneCapability(Message result) { RadioConfigProxy proxy = getRadioConfigProxy(null); if (proxy.isEmpty()) return; if (proxy.getVersion().less(RIL.RADIO_HAL_VERSION_1_1)) { if (result != null) { AsyncResult.forMessage(result, null, CommandException.fromRilErrno(REQUEST_NOT_SUPPORTED)); result.sendToTarget(); } return; } RILRequest rr = obtainRequest(RIL_REQUEST_GET_PHONE_CAPABILITY, result, mDefaultWorkSource); if (DBG) { logd(rr.serialString() + "> " + RILUtils.requestToString(rr.mRequest)); } try { proxy.getPhoneCapability(rr.mSerial); } catch (RemoteException | RuntimeException e) { resetProxyAndRequestList("getPhoneCapability", e); } } /** * @return whether current radio config version supports SET_PREFERRED_DATA_MODEM command. * If yes, we'll use RIL_REQUEST_SET_PREFERRED_DATA_MODEM to indicate which modem is preferred. * If not, we shall use RIL_REQUEST_ALLOW_DATA for on-demand PS attach / detach. * See PhoneSwitcher for more details. */ public boolean isSetPreferredDataCommandSupported() { RadioConfigProxy proxy = getRadioConfigProxy(null); return !proxy.isEmpty() && proxy.getVersion().greaterOrEqual(RIL.RADIO_HAL_VERSION_1_1); } /** * Wrapper function for IRadioConfig.setSimSlotsMapping(int32_t serial, vec slotMap). */ public void setSimSlotsMapping(List slotMapping, Message result) { RadioConfigProxy proxy = getRadioConfigProxy(result); if (proxy.isEmpty()) return; RILRequest rr = obtainRequest(RIL_REQUEST_SET_LOGICAL_TO_PHYSICAL_SLOT_MAPPING, result, mDefaultWorkSource); if (DBG) { logd(rr.serialString() + "> " + RILUtils.requestToString(rr.mRequest) + " " + slotMapping); } try { proxy.setSimSlotsMapping(rr.mSerial, slotMapping); } catch (RemoteException | RuntimeException e) { resetProxyAndRequestList("setSimSlotsMapping", e); } } /** * Wrapper function for using IRadioConfig.setNumOfLiveModems(int32_t serial, * byte numOfLiveModems) to switch between single-sim and multi-sim. */ public void setNumOfLiveModems(int numOfLiveModems, Message result) { RadioConfigProxy proxy = getRadioConfigProxy(result); if (proxy.isEmpty()) return; if (proxy.getVersion().less(RIL.RADIO_HAL_VERSION_1_1)) { if (result != null) { AsyncResult.forMessage( result, null, CommandException.fromRilErrno(REQUEST_NOT_SUPPORTED)); result.sendToTarget(); } return; } RILRequest rr = obtainRequest(RIL_REQUEST_SWITCH_DUAL_SIM_CONFIG, result, mDefaultWorkSource); if (DBG) { logd(rr.serialString() + "> " + RILUtils.requestToString(rr.mRequest) + ", numOfLiveModems = " + numOfLiveModems); } try { proxy.setNumOfLiveModems(rr.mSerial, numOfLiveModems); } catch (RemoteException | RuntimeException e) { resetProxyAndRequestList("setNumOfLiveModems", e); } } /** * Register a handler to get SIM slots that support simultaneous calling changed notifications. */ public void registerForSimultaneousCallingSupportStatusChanged(Handler h, int what, Object obj) { mSimultaneousCallingSupportStatusRegistrant = new Registrant(h, what, obj); } /** * Register a handler to get SIM slot status changed notifications. */ public void registerForSimSlotStatusChanged(Handler h, int what, Object obj) { mSimSlotStatusRegistrant = new Registrant(h, what, obj); } /** * Unregister corresponding to registerForSimSlotStatusChanged(). */ public void unregisterForSimSlotStatusChanged(Handler h) { if (mSimSlotStatusRegistrant != null && mSimSlotStatusRegistrant.getHandler() == h) { mSimSlotStatusRegistrant.clear(); mSimSlotStatusRegistrant = null; } } /** * Gets the hal capabilities from the device. */ public void getHalDeviceCapabilities(Message result) { RadioConfigProxy proxy = getRadioConfigProxy(Message.obtain(result)); if (proxy.isEmpty()) return; if (proxy.getVersion().less(RIL.RADIO_HAL_VERSION_1_3)) { if (result != null) { if (DBG) { logd("RIL_REQUEST_GET_HAL_DEVICE_CAPABILITIES > REQUEST_NOT_SUPPORTED"); } AsyncResult.forMessage(result, /* Send response such that all capabilities are supported (depending on the hal version of course.) */ proxy.getFullCapabilitySet(), CommandException.fromRilErrno(REQUEST_NOT_SUPPORTED)); result.sendToTarget(); } else { if (DBG) { logd("RIL_REQUEST_GET_HAL_DEVICE_CAPABILITIES > REQUEST_NOT_SUPPORTED " + "on complete message not set."); } } return; } RILRequest rr = obtainRequest(RIL_REQUEST_GET_HAL_DEVICE_CAPABILITIES, result, mDefaultWorkSource); if (DBG) { logd(rr.serialString() + "> " + RILUtils.requestToString(rr.mRequest)); } try { proxy.getHalDeviceCapabilities(rr.mSerial); } catch (RemoteException | RuntimeException e) { resetProxyAndRequestList("getHalDeviceCapabilities", e); } } /** * Returns the device's nr capability. */ public int[] getDeviceNrCapabilities() { return mDeviceNrCapabilities; } private static void logd(String log) { Rlog.d(TAG, log); } private static void loge(String log) { Rlog.e(TAG, log); } @Override public String toString() { return "RadioConfig[" + "mRadioConfigProxy=" + mRadioConfigProxy + ']'; } }