导出联系人到SIM流程分析

655 阅读7分钟

说明

文章还没写完,最后一步IRradio.iccIOForApp方法的具体调用暂时未查找到,如有大佬能补充下,感激不尽

参考文献

  1. subid和slotid_firedancer0089的专栏-CSDN博客
  2. Android:关于ContentProvider的知识都在这里了!_专注分享 Android开发 干货-CSDN博客_contentprovider
  3. ADN加载流程_Android开发 - UCloud云社区
  4. Android:全面&详细解析android 10 RSSI信号格显示与刷新其二,RIL篇,重点类与文件:RIL.java,ril_service.cpp,radioResponse.java_GentelmanTsao的博客-CSDN博客

前言

最近收到个bug,名字过长,导出SIM失败,了解了下SIM卡存储以及Android设备联系人 导入到SIM流程

SIM存储限制

(SIM标准中文应该是只有4个)

  1. SIM卡记录名字的最大长度是14,中文可以输入6个,(同样的中文可以输入11个,如 天天天天天天天天天天天,但是只要有一个不一样的中文就只能有6个)
  2. 号码长度过长

手机号码导入SIM卡流程记录(MTK)

1. 应用层调用

Mtk SimContacts 应用代码 SimContactDaoImpl.java


class SimContactDaoImpl extends SimContactDao{
    ...
    public static final Uri ICC_CONTENT_URI = Uri.parse("content://icc/adn");
    public static String NUMBER = "number";
    public static String EMAILS = "emails";
    public static String ANRS = "anrs";
    private static final String STR_TAG = "tag";
    @Override
    public Uri createNewSimContact(SimContact simContact, int subscriptionId) {
        // 代码1
        Uri uri = ICC_CONTENT_URI.buildUpon()
                .appendPath("subId")
                .appendPath(String.valueOf(subscriptionId))
                .build();
        ContentValues values = new ContentValues();
        values.put(STR_TAG, TextUtils.isEmpty(simContact.getName()) ? "" : simContact.getName());
        if (!TextUtils.isEmpty(simContact.getPhone())) {
            values.put(NUMBER, PhoneNumberUtils.stripSeparators(simContact.getPhone()));
        }
        values.put(EMAILS, simContact.getEmailsString());
        values.put(ANRS, simContact.getAnrsString().
                replaceAll("[^0123456789PWN\\,\\;\\*\\#\\+\\:]", ""));
        return mResolver.insert(uri,values);//调用
    }
    ...
    
}
  • 从以上代码可以看出,对SIM卡的操作是发出一个Uri,通过ContentProvider进行进程通信,修改sim信息,其传入了 STR_TAG(名称),NUMBER、EMAILS、ANRS等字段,且 uri中传入了subId

注: subId 是卡在 数据库对应的主键值,可以通过subId查询对应卡槽。

IccProvider

ICC_CONTENT_URI对应的ContentProviderIccProvider(path:/frameworks/opt/telephony/src/java/com/android/internal/telephony/IccProvider.java)

IccProvider#insert

public Uri insert(Uri url, ContentValues initialValues) {
    Uri resultUri;
    int efType;
    String pin2 = null;
    int subId;
    int match = URL_MATCHER.match(url);
    switch (match) {
        ...
        case ADN_SUB:
                efType = IccConstants.EF_ADN;
                subId = getRequestSubId(url);
                break;
        ...
    }

    String tag = initialValues.getAsString("tag");
    String number = initialValues.getAsString("number");
    // TODO(): Read email instead of sending null.
    // 代码1 调用addIccRecordToEf 将数据添加到sim卡
    boolean success = addIccRecordToEf(efType, tag, number, null, pin2, subId);

    if (!success) {
        return null;
    }

    StringBuilder buf = new StringBuilder("content://icc/");
    switch (match) {
        ...
        case ADN_SUB:
            buf.append("adn/subId/");
            break;
        ...
    }

    // TODO: we need to find out the rowId for the newly added record
    buf.append(0);

    resultUri = Uri.parse(buf.toString());

    getContext().getContentResolver().notifyChange(url, null);
    /*
    // notify interested parties that an insertion happened
    getContext().getContentResolver().notifyInsert(
            resultUri, rowID, null);
    */

    return resultUri;
}
  • 代码1处调用addIccRecordToEf方法将数据添加到sim卡

IccProvider#addIccRecordToEf

private boolean
    addIccRecordToEf(int efType, String name, String number, String[] emails,
            String pin2, int subId) {
        ...
        boolean success = false;
        ...
        try {
            //代码1
            IIccPhoneBook iccIpb = IIccPhoneBook.Stub.asInterface(
                    ServiceManager.getService("simphonebook"));
            if (iccIpb != null) {
                success = iccIpb.updateAdnRecordsInEfBySearchForSubscriber(subId, efType,
                        "", "", name, number, pin2);//
            }
        } catch (RemoteException ex) {
            // ignore it
        } catch (SecurityException ex) {
            if (DBG) log(ex.toString());
        }
        if (DBG) log("addIccRecordToEf: " + success);
        return success;
    }

  • 从上面代码中可以看出 addIccRecordToEf方法通过binder调用 simphonebook服务的 updateAdnRecordsInEfBySearchForSubscriber方法来添加sim信息。 simphonebook 服务的实现类是 UiccPhoneBookController.java

simphonebook 服务

UiccPhoneBookController.java代码如下

public class UiccPhoneBookController extends IIccPhoneBook.Stub{
    ...
     @Override
    public boolean
    updateAdnRecordsInEfBySearch (int efid, String oldTag, String oldPhoneNumber,
            String newTag, String newPhoneNumber, String pin2) throws android.os.RemoteException {
        return updateAdnRecordsInEfBySearchForSubscriber(getDefaultSubscription(), efid, oldTag,
                oldPhoneNumber, newTag, newPhoneNumber, pin2);
    }
       @Override
    public boolean
    updateAdnRecordsInEfBySearchForSubscriber(int subId, int efid, String oldTag,
            String oldPhoneNumber, String newTag, String newPhoneNumber,
            String pin2) throws android.os.RemoteException {
        IccPhoneBookInterfaceManager iccPbkIntMgr =
                             getIccPhoneBookInterfaceManager(subId);
        if (iccPbkIntMgr != null) {
            return iccPbkIntMgr.updateAdnRecordsInEfBySearch(efid, oldTag,
                    oldPhoneNumber, newTag, newPhoneNumber, pin2);
        } else {
            Rlog.e(TAG,"updateAdnRecordsInEfBySearch iccPbkIntMgr is" +
                      " null for Subscription:"+subId);
            return false;
        }
    }
    
   /**
     * get phone book interface manager object based on subscription.
     **/
    private IccPhoneBookInterfaceManager
            getIccPhoneBookInterfaceManager(int subId) {
        // 代码1
        int phoneId = SubscriptionController.getInstance().getPhoneId(subId);
        try {
            //代码2
            return mPhone[phoneId].getIccPhoneBookInterfaceManager();
        } catch (NullPointerException e) {
            Rlog.e(TAG, "Exception is :"+e.toString()+" For subscription :"+subId );
            e.printStackTrace(); //To print stack trace
            return null;
        } catch (ArrayIndexOutOfBoundsException e) {
            Rlog.e(TAG, "Exception is :"+e.toString()+" For subscription :"+subId );
            e.printStackTrace();
            return null;
        }
    }
}
  • 代码1处通过subId获取对应 phoneId(此处代码就不分析了)
  • 代码2处获取了phoneId后,根据phoneId 获取Phone对象(mPhone是一个Phone数组),调用 getIccPhoneBookInterfaceManager方法获得IccPhoneBookInterfaceManager对象。
  • 代码3 调用IccPhoneBookInterfaceManager#updateAdnRecordsInEfBySearch方法更新sim卡存储联系人信息。

/frameworks/opt/telephony/src/java/com/android/internal/telephony/IccPhoneBookInterfaceManager.java IccPhoneBookInterfaceManager.java#updateAdnRecordsInEfBySearch代码如下:

 public boolean
    updateAdnRecordsInEfBySearch (int efid,
            String oldTag, String oldPhoneNumber,
            String newTag, String newPhoneNumber, String pin2) {
        //代码1 检查权限
        if (mPhone.getContext().checkCallingOrSelfPermission(
                android.Manifest.permission.WRITE_CONTACTS)
            != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException(
                    "Requires android.permission.WRITE_CONTACTS permission");
        }
        ...

        efid = updateEfForIccType(efid);//不太清楚作用
        synchronized(mLock) {
            checkThread();
            mSuccess = false;
            AtomicBoolean status = new AtomicBoolean(false);
            Message response = mBaseHandler.obtainMessage(EVENT_UPDATE_DONE, status);
            AdnRecord oldAdn = new AdnRecord(oldTag, oldPhoneNumber);
            AdnRecord newAdn = new AdnRecord(newTag, newPhoneNumber);
            if (mAdnCache != null) {
                //代码2
                mAdnCache.updateAdnBySearch(efid, oldAdn, newAdn, pin2, response);
                //代码3
                waitForResult(status);
            } else {
                loge("Failure while trying to update by search due to uninitialised adncache");
            }
        }
        return mSuccess;
    }
    private int updateEfForIccType(int efid) {
        // Check if we are trying to read ADN records
        if (efid == IccConstants.EF_ADN) {
            if (mPhone.getCurrentUiccAppType() == AppType.APPTYPE_USIM) {
                return IccConstants.EF_PBR;
            }
        }
        return efid;
    }
    protected void waitForResult(AtomicBoolean status) {
        while (!status.get()) {
            try {
                mLock.wait();
            } catch (InterruptedException e) {
                logd("interrupted while trying to update by search");
            }
        }
    }
  • 代码2 处调用AdnRecordCache.java#updateAdnBySearch方法,写入值
  • 代码3 处调用waitForResult()方法阻塞线程,等待 status 被置为true.
    private IccFileHandler mFh;//文件操作Handler
    /**
     * Replace oldAdn with newAdn in ADN-like record in EF
     *
     * The ADN-like records must be read through requestLoadAllAdnLike() before
     *
     * @param efid must be one of EF_ADN, EF_FDN, and EF_SDN
     * @param oldAdn is the adn to be replaced
     *        If oldAdn.isEmpty() is ture, it insert the newAdn
     * @param newAdn is the adn to be stored
     *        If newAdn.isEmpty() is true, it delete the oldAdn
     * @param pin2 is required to update EF_FDN, otherwise must be null
     * @param response message to be posted when done
     *        response.exception hold the exception in error
     */
    public void updateAdnBySearch(int efid, AdnRecord oldAdn, AdnRecord newAdn,
            String pin2, Message response) {

        int extensionEF;
        extensionEF = extensionEfForEf(efid);

        if (extensionEF < 0) {
            sendErrorResponse(response, "EF is not known ADN-like EF:0x" +
                    Integer.toHexString(efid).toUpperCase());
            return;
        }

        ArrayList<AdnRecord>  oldAdnList;

        if (efid == EF_PBR) {
            oldAdnList = mUsimPhoneBookManager.loadEfFilesFromUsim();
        } else {
            oldAdnList = getRecordsIfLoaded(efid);
        }

        if (oldAdnList == null) {
            sendErrorResponse(response, "Adn list not exist for EF:0x" +
                    Integer.toHexString(efid).toUpperCase());
            return;
        }

        int index = -1;
        int count = 1;
        //代码1 遍历查询要被替代的联系人信息位置(增加联系人时,传入的是"")
        for (Iterator<AdnRecord> it = oldAdnList.iterator(); it.hasNext(); ) {
            if (oldAdn.isEqual(it.next())) {
                index = count;
                break;
            }
            count++;
        }

        if (index == -1) {
            sendErrorResponse(response, "Adn record don't exist for " + oldAdn);
            return;
        }

        if (efid == EF_PBR) {
            AdnRecord foundAdn = oldAdnList.get(index-1);
            efid = foundAdn.mEfid;
            extensionEF = foundAdn.mExtRecord;
            index = foundAdn.mRecordNumber;

            newAdn.mEfid = efid;
            newAdn.mExtRecord = extensionEF;
            newAdn.mRecordNumber = index;
        }

        Message pendingResponse = mUserWriteResponse.get(efid);

        if (pendingResponse != null) {
            sendErrorResponse(response, "Have pending update for EF:0x" +
                    Integer.toHexString(efid).toUpperCase());
            return;
        }

        mUserWriteResponse.put(efid, response);
        //代码2
        new AdnRecordLoader(mFh).updateEF(newAdn, efid, extensionEF,
                index, pin2,
                obtainMessage(EVENT_UPDATE_ADN_DONE, efid, index, newAdn));
    }
    
    public ArrayList<AdnRecord> getRecordsIfLoaded(int efid) {
        return mAdnLikeFiles.get(efid);
    }
  • 代码1 代码1 遍历查询要被替代的联系人信息位置,由IccProvider#addIccRecordToEf处代码可以看出传入的number和phone都是 "".
  • 代码2 处调用 AdnRecordLoader.java#updateEF方法更新联系人信息
    private IccFileHandler mFh;
    AdnRecordLoader(IccFileHandler fh) {
        // The telephony unit-test cases may create AdnRecords
        // in secondary threads
        super(Looper.getMainLooper());
        mFh = fh;
    }
    /**
     * Write adn to a EF SIM record
     * It will get the record size of EF record and compose hex adn array
     * then write the hex array to EF record
     *
     * @param adn is set with alphaTag and phone number
     * @param ef EF fileid
     * @param extensionEF extension EF fileid
     * @param recordNumber 1-based record index
     * @param pin2 for CHV2 operations, must be null if pin2 is not needed
     * @param response will be sent to its handler when completed
     */
    public void
    updateEF(AdnRecord adn, int ef, int extensionEF, int recordNumber,
            String pin2, Message response) {
        mEf = ef;
        mExtensionEF = extensionEF;
        mRecordNumber = recordNumber;
        mUserResponse = response;
        mPin2 = pin2;
        //代码1
        mFh.getEFLinearRecordSize( ef, getEFPath(ef),
                obtainMessage(EVENT_EF_LINEAR_RECORD_SIZE_DONE, adn));
     }
     
     ...
  • 代码1处调用IccFileHandler.java#java方法,传入 根据efid 值获取对着应操作sim卡的文件路径。(getEFPath方法在IccFileHandle.java 中是个abstract方法,对于sim来说,一般其实现为SIMFileHandler.java) IccFileHandler.java#getEFLinearRecordSize
    protected final CommandsInterface mCi;
    /**
     * get record size for a linear fixed EF
     *
     * @param fileid EF id
     * @param path Path of the EF on the card
     * @param onLoaded ((AsnyncResult)(onLoaded.obj)).result is the recordSize[]
     *        int[0] is the record length int[1] is the total length of the EF
     *        file int[3] is the number of records in the EF file So int[0] *
     *        int[3] = int[1]
     */
    public void getEFLinearRecordSize(int fileid, String path, Message onLoaded) {
        String efPath = (path == null) ? getEFPath(fileid) : path;
        Message response
                = obtainMessage(EVENT_GET_EF_LINEAR_RECORD_SIZE_DONE,
                        new LoadLinearFixedContext(fileid, efPath, onLoaded));
        //代码1
        mCi.iccIOForApp(COMMAND_GET_RESPONSE, fileid, efPath,
                    0, 0, GET_RESPONSE_EF_SIZE_BYTES, null, null, mAid, response);
    }
  • 代码1处调用iccIOForApp 方法

/frameworks/opt/telephony/src/java/com/android/internal/telephony/CommandsInterface.java

其实现类是/frameworks/opt/telephony/src/java/com/android/internal/telephony/RIL.java RIL.java代码

    static final String[] HIDL_SERVICE_NAME = {"slot1", "slot2", "slot3"};
@Override
    public void iccIOForApp(int command, int fileId, String path, int p1, int p2, int p3,
                 String data, String pin2, String aid, Message result) {
        //代码1
        IRadio radioProxy = getRadioProxy(result);
        if (radioProxy != null) {
            RILRequest rr = obtainRequest(RIL_REQUEST_SIM_IO, result,
                    mRILDefaultWorkSource);

            if (RILJ_LOGD) {
                if (Build.IS_DEBUGGABLE) {
                    riljLog(rr.serialString() + "> iccIO: "
                            + requestToString(rr.mRequest) + " command = 0x"
                            + Integer.toHexString(command) + " fileId = 0x"
                            + Integer.toHexString(fileId) + " path = " + path + " p1 = "
                            + p1 + " p2 = " + p2 + " p3 = " + " data = " + data
                            + " aid = " + aid);
                } else {
                    riljLog(rr.serialString() + "> iccIO: " + requestToString(rr.mRequest));
                }
            }

            IccIo iccIo = new IccIo();
            iccIo.command = command;
            iccIo.fileId = fileId;
            iccIo.path = convertNullToEmptyString(path);
            iccIo.p1 = p1;
            iccIo.p2 = p2;
            iccIo.p3 = p3;
            iccIo.data = convertNullToEmptyString(data);
            iccIo.pin2 = convertNullToEmptyString(pin2);
            iccIo.aid = convertNullToEmptyString(aid);

            try {
                //代码4 
                radioProxy.iccIOForApp(rr.mSerial, iccIo);
            } catch (RemoteException | RuntimeException e) {
                handleRadioProxyExceptionForRR(rr, "iccIOForApp", e);
            }
        }
    }
    
    /** Returns a {@link IRadio} instance or null if the service is not available. */
    @VisibleForTesting
    public IRadio getRadioProxy(Message result) {
        //代码2 判断此时是否有移动网络
        if (!mIsMobileNetworkSupported) {
            if (RILJ_LOGV) riljLog("getRadioProxy: Not calling getService(): wifi-only");
            if (result != null) {
                AsyncResult.forMessage(result, null,
                        CommandException.fromRilErrno(RADIO_NOT_AVAILABLE));
                result.sendToTarget();
            }
            return null;
        }

        if (mRadioProxy != null) {
            return mRadioProxy;
        }

        try {
            //代码3 根据phoneId获取对应卡槽,默认为卡槽1
            mRadioProxy = IRadio.getService(HIDL_SERVICE_NAME[mPhoneId == null ? 0 : mPhoneId],
                    true);
            if (mRadioProxy != null) {
                mRadioProxy.linkToDeath(mRadioProxyDeathRecipient,
                        mRadioProxyCookie.incrementAndGet());
                mRadioProxy.setResponseFunctions(mRadioResponse, mRadioIndication);
            } else {
                riljLoge("getRadioProxy: mRadioProxy == null");
            }
        } catch (RemoteException | RuntimeException e) {
            mRadioProxy = null;
            riljLoge("RadioProxy getService/setResponseFunctions: " + e);
        }

        if (mRadioProxy == null) {
            // getService() is a blocking call, so this should never happen
            riljLoge("getRadioProxy: mRadioProxy == null");
            if (result != null) {
                AsyncResult.forMessage(result, null,
                        CommandException.fromRilErrno(RADIO_NOT_AVAILABLE));
                result.sendToTarget();
            }
        }

        return mRadioProxy;
    }
  • 代码1 获取Radio服务
  • 代码2 判断是否支持移动网络
  • 代码3 根据phoneId获取卡槽,再根据卡槽获取对应卡槽的服务类,IRadio.getService获取的实现类是/hardware/ril/libril/ril_service.cpp?

...