content: /*
   * 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 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_PHONE_CAPABILITY;
  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.hardware.radio.V1_0.RadioResponseInfo;
  import android.hardware.radio.V1_0.RadioResponseType;
  import android.hardware.radio.config.V1_0.IRadioConfig;
  import android.hardware.radio.config.V1_1.ModemsConfig;
  import android.net.ConnectivityManager;
  import android.os.AsyncResult;
  import android.os.Handler;
  import android.os.HwBinder;
  import android.os.Message;
  import android.os.Registrant;
  import android.os.RemoteException;
  import android.os.WorkSource;
  import android.telephony.Rlog;
  import android.util.SparseArray;
  
  import com.android.internal.telephony.uicc.IccSlotStatus;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  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 int EVENT_SERVICE_DEAD = 1;
  
      private static final HalVersion RADIO_CONFIG_HAL_VERSION_UNKNOWN = new HalVersion(-1, -1);
  
      private static final HalVersion RADIO_CONFIG_HAL_VERSION_1_0 = new HalVersion(1, 0);
  
      private static final HalVersion RADIO_CONFIG_HAL_VERSION_1_1 = new HalVersion(1, 1);
  
      private final boolean mIsMobileNetworkSupported;
      private volatile IRadioConfig mRadioConfigProxy = null;
      // IRadioConfig version
      private HalVersion mRadioConfigVersion = RADIO_CONFIG_HAL_VERSION_UNKNOWN;
      private final ServiceDeathRecipient mServiceDeathRecipient;
      private final AtomicLong mRadioConfigProxyCookie = new AtomicLong(0);
      private final RadioConfigResponse mRadioConfigResponse;
      private final RadioConfigIndication mRadioConfigIndication;
      private final SparseArray<RILRequest> mRequestList = new SparseArray<RILRequest>();
      /* default work source which will blame phone process */
      private final WorkSource mDefaultWorkSource;
      private static RadioConfig sRadioConfig;
  
      protected Registrant mSimSlotStatusRegistrant;
  
      final class ServiceDeathRecipient implements HwBinder.DeathRecipient {
          @Override
          public void serviceDied(long cookie) {
              // Deal with service going away
              logd("serviceDied");
              sendMessage(obtainMessage(EVENT_SERVICE_DEAD, cookie));
          }
      }
  
      private RadioConfig(Context context) {
          ConnectivityManager cm = (ConnectivityManager) context.getSystemService(
                  Context.CONNECTIVITY_SERVICE);
          mIsMobileNetworkSupported = cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE);
  
          mRadioConfigResponse = new RadioConfigResponse(this);
          mRadioConfigIndication = new RadioConfigIndication(this);
          mServiceDeathRecipient = new ServiceDeathRecipient();
  
          mDefaultWorkSource = new WorkSource(context.getApplicationInfo().uid,
                  context.getPackageName());
      }
  
      /**
       * Returns the singleton static instance of RadioConfig
       */
      public static RadioConfig getInstance(Context context) {
          if (sRadioConfig == null) {
              sRadioConfig = new RadioConfig(context);
          }
          return sRadioConfig;
      }
  
      @Override
      public void handleMessage(Message message) {
          switch (message.what) {
              case EVENT_SERVICE_DEAD:
                  logd("handleMessage: EVENT_SERVICE_DEAD cookie = " + message.obj
                          + " mRadioConfigProxyCookie = " + mRadioConfigProxyCookie.get());
                  if ((long) message.obj == mRadioConfigProxyCookie.get()) {
                      resetProxyAndRequestList("EVENT_SERVICE_DEAD", null);
                  }
                  break;
          }
      }
  
      /**
       * 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 + "] " + requestToString(rr.mRequest));
                  }
                  rr.onError(error, null);
                  rr.release();
              }
              mRequestList.clear();
          }
      }
  
      private void resetProxyAndRequestList(String caller, Exception e) {
          loge(caller + ": " + e);
          mRadioConfigProxy = null;
  
          // 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 {@link IRadioConfig} instance or null if the service is not available. */
      public IRadioConfig 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();
              }
              return null;
          }
  
          if (mRadioConfigProxy != null) {
              return mRadioConfigProxy;
          }
  
          updateRadioConfigProxy();
  
          if (mRadioConfigProxy == null) {
              if (result != null) {
                  AsyncResult.forMessage(result, null,
                          CommandException.fromRilErrno(RADIO_NOT_AVAILABLE));
                  result.sendToTarget();
              }
          }
  
          return mRadioConfigProxy;
      }
  
      private void updateRadioConfigProxy() {
          try {
              // Try to get service from different versions.
              try {
                  mRadioConfigProxy = android.hardware.radio.config.V1_1.IRadioConfig.getService(
                          true);
                  mRadioConfigVersion = RADIO_CONFIG_HAL_VERSION_1_1;
              } catch (NoSuchElementException e) {
              }
  
              if (mRadioConfigProxy == null) {
                  try {
                      mRadioConfigProxy = android.hardware.radio.config.V1_0
                              .IRadioConfig.getService(true);
                      mRadioConfigVersion = RADIO_CONFIG_HAL_VERSION_1_0;
                  } catch (NoSuchElementException e) {
                  }
              }
  
              if (mRadioConfigProxy == null) {
                  loge("getRadioConfigProxy: mRadioConfigProxy == null");
                  return;
              }
  
              // Link to death recipient and set response. If fails, set proxy to null and return.
              mRadioConfigProxy.linkToDeath(mServiceDeathRecipient,
                      mRadioConfigProxyCookie.incrementAndGet());
              mRadioConfigProxy.setResponseFunctions(mRadioConfigResponse,
                      mRadioConfigIndication);
          } catch (RemoteException | RuntimeException e) {
              mRadioConfigProxy = null;
              loge("getRadioConfigProxy: RadioConfigProxy setResponseFunctions: " + e);
              return;
          }
      }
  
      private RILRequest obtainRequest(int request, Message result, WorkSource workSource) {
          RILRequest rr = RILRequest.obtain(request, result, workSource);
          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) {
                  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(RadioResponseInfo responseInfo) {
          int serial = responseInfo.serial;
          int error = responseInfo.error;
          int type = responseInfo.type;
  
          if (type != 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) {
          IRadioConfig radioConfigProxy = getRadioConfigProxy(result);
          if (radioConfigProxy != null) {
              RILRequest rr = obtainRequest(RIL_REQUEST_GET_SLOT_STATUS, result, mDefaultWorkSource);
  
              if (DBG) {
                  logd(rr.serialString() + "> " + requestToString(rr.mRequest));
              }
  
              try {
                  radioConfigProxy.getSimSlotsStatus(rr.mSerial);
              } catch (RemoteException | RuntimeException e) {
                  resetProxyAndRequestList("getSimSlotsStatus", e);
              }
          }
      }
  
      /**
       * Wrapper function for IRadioConfig.setPreferredDataModem(int modemId).
       */
      public void setPreferredDataModem(int modemId, Message result) {
          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() + "> " + requestToString(rr.mRequest));
          }
  
          try {
              ((android.hardware.radio.config.V1_1.IRadioConfig) mRadioConfigProxy)
                      .setPreferredDataModem(rr.mSerial, (byte) modemId);
          } catch (RemoteException | RuntimeException e) {
              resetProxyAndRequestList("setPreferredDataModem", e);
          }
      }
  
      /**
       * Wrapper function for IRadioConfig.getPhoneCapability().
       */
      public void getPhoneCapability(Message result) {
          IRadioConfig radioConfigProxy = getRadioConfigProxy(null);
          if (radioConfigProxy == null || mRadioConfigVersion.less(RADIO_CONFIG_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() + "> " + requestToString(rr.mRequest));
          }
  
          try {
              ((android.hardware.radio.config.V1_1.IRadioConfig) mRadioConfigProxy)
                      .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() {
          IRadioConfig radioConfigProxy = getRadioConfigProxy(null);
          return radioConfigProxy != null && mRadioConfigVersion
                  .greaterOrEqual(RADIO_CONFIG_HAL_VERSION_1_1);
      }
  
      /**
       * Wrapper function for IRadioConfig.setSimSlotsMapping(int32_t serial, vec<uint32_t> slotMap).
       */
      public void setSimSlotsMapping(int[] physicalSlots, Message result) {
          IRadioConfig radioConfigProxy = getRadioConfigProxy(result);
          if (radioConfigProxy != null) {
              RILRequest rr = obtainRequest(RIL_REQUEST_SET_LOGICAL_TO_PHYSICAL_SLOT_MAPPING, result,
                      mDefaultWorkSource);
  
              if (DBG) {
                  logd(rr.serialString() + "> " + requestToString(rr.mRequest)
                          + " " + Arrays.toString(physicalSlots));
              }
  
              try {
                  radioConfigProxy.setSimSlotsMapping(rr.mSerial,
                          primitiveArrayToArrayList(physicalSlots));
              } catch (RemoteException | RuntimeException e) {
                  resetProxyAndRequestList("setSimSlotsMapping", e);
              }
          }
      }
  
      private static ArrayList<Integer> primitiveArrayToArrayList(int[] arr) {
          ArrayList<Integer> arrayList = new ArrayList<>(arr.length);
          for (int i : arr) {
              arrayList.add(i);
          }
          return arrayList;
      }
  
      static String requestToString(int request) {
          switch (request) {
              case RIL_REQUEST_GET_PHONE_CAPABILITY:
                  return "GET_PHONE_CAPABILITY";
              case RIL_REQUEST_GET_SLOT_STATUS:
                  return "GET_SLOT_STATUS";
              case RIL_REQUEST_SET_LOGICAL_TO_PHYSICAL_SLOT_MAPPING:
                  return "SET_LOGICAL_TO_PHYSICAL_SLOT_MAPPING";
              case RIL_REQUEST_SET_PREFERRED_DATA_MODEM:
                  return "SET_PREFERRED_DATA_MODEM";
              case RIL_REQUEST_SWITCH_DUAL_SIM_CONFIG:
                  return "SWITCH_DUAL_SIM_CONFIG";
              default:
                  return "<unknown request " + request + ">";
          }
      }
  
      /**
       * Wrapper function for using IRadioConfig.setModemsConfig(int32_t serial,
       * ModemsConfig modemsConfig) to switch between single-sim and multi-sim.
       */
      public void setModemsConfig(int numOfLiveModems, Message result) {
          IRadioConfig radioConfigProxy = getRadioConfigProxy(result);
          if (radioConfigProxy != null
                  && mRadioConfigVersion.greaterOrEqual(RADIO_CONFIG_HAL_VERSION_1_1)) {
              android.hardware.radio.config.V1_1.IRadioConfig radioConfigProxy11 =
                      (android.hardware.radio.config.V1_1.IRadioConfig) radioConfigProxy;
              RILRequest rr = obtainRequest(RIL_REQUEST_SWITCH_DUAL_SIM_CONFIG,
                      result, mDefaultWorkSource);
  
              if (DBG) {
                  logd(rr.serialString() + "> " + requestToString(rr.mRequest)
                          + ", numOfLiveModems = " + numOfLiveModems);
              }
  
              try {
                  ModemsConfig modemsConfig = new ModemsConfig();
                  modemsConfig.numOfLiveModems = (byte) numOfLiveModems;
                  radioConfigProxy11.setModemsConfig(rr.mSerial, modemsConfig);
              } catch (RemoteException | RuntimeException e) {
                  resetProxyAndRequestList("setModemsConfig", e);
              }
          }
      }
  
      // TODO: not needed for now, but if we don't want to use System Properties any more,
      // we need to implement a wrapper function for getModemsConfig as well
  
      /**
       * 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;
          }
      }
  
      static ArrayList<IccSlotStatus> convertHalSlotStatus(
              ArrayList<android.hardware.radio.config.V1_0.SimSlotStatus> halSlotStatusList) {
          ArrayList<IccSlotStatus> response = new ArrayList<IccSlotStatus>(halSlotStatusList.size());
          for (android.hardware.radio.config.V1_0.SimSlotStatus slotStatus : halSlotStatusList) {
              IccSlotStatus iccSlotStatus = new IccSlotStatus();
              iccSlotStatus.setCardState(slotStatus.cardState);
              iccSlotStatus.setSlotState(slotStatus.slotState);
              iccSlotStatus.logicalSlotIndex = slotStatus.logicalSlotId;
              iccSlotStatus.atr = slotStatus.atr;
              iccSlotStatus.iccid = slotStatus.iccid;
              response.add(iccSlotStatus);
          }
          return response;
      }
  
      static ArrayList<IccSlotStatus> convertHalSlotStatus_1_2(
              ArrayList<android.hardware.radio.config.V1_2.SimSlotStatus> halSlotStatusList) {
          ArrayList<IccSlotStatus> response = new ArrayList<IccSlotStatus>(halSlotStatusList.size());
          for (android.hardware.radio.config.V1_2.SimSlotStatus slotStatus : halSlotStatusList) {
              IccSlotStatus iccSlotStatus = new IccSlotStatus();
              iccSlotStatus.setCardState(slotStatus.base.cardState);
              iccSlotStatus.setSlotState(slotStatus.base.slotState);
              iccSlotStatus.logicalSlotIndex = slotStatus.base.logicalSlotId;
              iccSlotStatus.atr = slotStatus.base.atr;
              iccSlotStatus.iccid = slotStatus.base.iccid;
              iccSlotStatus.eid = slotStatus.eid;
              response.add(iccSlotStatus);
          }
          return response;
      }
  
      private static void logd(String log) {
          Rlog.d(TAG, log);
      }
  
      private static void loge(String log) {
          Rlog.e(TAG, log);
      }
  }
  