本文代碼以MTK平台Android 4.4為分析對象,與Google原生AOSP有些許差異,請讀者知悉。
前置文章:
《 Android 4.4 Kitkat Phone工作流程淺析(一)__概要和學習計劃 》
《Android 4.4 Kitkat Phone工作流程淺析(二)__UI結構分析》
《Android 4.4 Kitkat Phone工作流程淺析(三)__MO(去電)流程分析》
《Android 4.4 Kitkat Phone工作流程淺析(四)__RILJ工作流程簡析》
《Android 4.4 Kitkat Phone工作流程淺析(五)__MT(來電)流程分析》
《Android 4.4 Kitkat Phone工作流程淺析(六)__InCallActivity顯示更新流程》
《Android 4.4 Kitkat Phone工作流程淺析(七)__來電(MT)響鈴流程》
《Android 4.4 Kitkat Phone工作流程淺析(八)__Phone狀态分析》
《Android 4.4 Kitkat Phone工作流程淺析(九)__狀态通知流程分析》
概要
無論是在MT (Mobile Termination Call被叫——來電),還是MO (Mobile Origination Call主叫——去電) 流程中,通話界面上都會顯示目前通話的名稱( 後文以displayName指代 )。通常情況下,如果是一個陌生号碼,則會顯示為該陌生号碼。如果是已知聯系人,則會顯示該聯系人的名稱。當然,在會議電話( Conference Call )的情況下則直接顯示"會議電話"。但是,在某些特殊情況下,displayName還會顯示諸如"私人号碼"、"公用電話"、"未知号碼"等。
本文主要分析displayName的擷取顯示流程及顯示"未知号碼"的原因,如圖1:

圖 1 通話界面顯示Unknown
查詢流程
開始查詢——CallCardPresenter
displayName是隸屬于CallCardFragment的控件,當通話MO/MT流程發起時InCallActivity會顯示,此時将會觸發CallCardFragment界面更新,在CallCardPresenter的init方法中查詢displayName,關鍵代碼如下:
[java] view plain copy
- public void init(Context context, Call call) {
- //... ...省略
- if (call != null) {
- mPrimary = call;
- final CallIdentification identification = call.getIdentification();
- // start processing lookups right away.
- if (!call.isConferenceCall()) {
- //... ...如果不是ConferenceCall就執行
- startContactInfoSearch(identification, PRIMARY,
- call.getState() == Call.State.INCOMING);
- } else {
- //... ..如果是ConferenceCall則執行
- updateContactEntry(null, PRIMARY, true);
- }
- }
- }
startContactInfoSearch的具體代碼如下: [java] view plain copy
- private void startContactInfoSearch(final CallIdentification identification,
- final boolean isPrimary, boolean isIncoming) {
- final int type, boolean isIncoming) {
- final ContactInfoCache cache = ContactInfoCache.getInstance(mContext);
- // ContactInfoCache中開始查找
- cache.findInfo(identification, isIncoming, new ContactInfoCacheCallback() {
- @Override
- public void onContactInfoComplete(int callId, ContactCacheEntry entry) {
- // 當查詢完畢之後回調并更新ContactEntry,這裡最終會去更新界面顯示
- updateContactEntry(entry, type, false);
- if (entry.name != null) {
- Log.d(TAG, "Contact found: " + entry);
- }
- if (entry.personUri != null) {
- CallerInfoUtils.sendViewNotification(mContext, entry.personUri);
- }
- }
- @Override
- public void onImageLoadComplete(int callId, ContactCacheEntry entry) {
- if (getUi() == null) {
- return;
- }
- if (entry.photo != null) {
- if (mPrimary != null && callId == mPrimary.getCallId()) {
- // 設定第一路通話頭像
- getUi().setPrimaryImage(entry.photo);
- } else if (mSecondary != null && callId == mSecondary.getCallId()) {
- // 設定第二路通話頭像
- getUi().setSecondaryImage(entry.photo);
- }
- }
- }
- });
- }
異步查詢——ContactInfoCache
在CallCardPresenter中發起查詢之後會跳轉到ContactInfoCache.findInfo()方法中,ContactInfoCache不僅用于查詢目前通話的相關資訊,還可以将這些資訊緩存以備下次查詢相同資訊時快速傳回。findInfo關鍵代碼如下:
[java] view plain copy
- public void findInfo(final CallIdentification identification, final boolean isIncoming,
- ContactInfoCacheCallback callback) {
- //... ...省略
- // 查詢caller資訊,完成之後會回調到FindInfoCallback中,會調用findInfoQueryComplete
- final CallerInfo callerInfo = CallerInfoUtils.getCallerInfoForCall(
- mContext, identification, new FindInfoCallback(isIncoming));
- if(!mExpiredInfoMap.containsKey(callId)) {
- // 查詢資訊完成之後執行
- findInfoQueryComplete(identification, callerInfo, isIncoming, false);
- }
- }
CallerInfo中包含了目前call的基本資訊,比如号碼、類型、特殊相關服務等,在擷取到這些資訊之後再進行進一步的聯系人資料庫查詢。
擷取CallerInfo——CallerInfoUtils
在getCallerInfoForCall()方法中,除了擷取目前Call的基本資訊之外,還會根據目前Call的phoneNumber去資料庫中查詢,關鍵代碼如下:
[java] view plain copy
- public static CallerInfo getCallerInfoForCall(Context context, CallIdentification call,
- CallerInfoAsyncQuery.OnQueryCompleteListener listener) {
- // 擷取目前Call的基本資訊并建立CallerInfo對象
- CallerInfo info = buildCallerInfo(context, call);
- String number = info.phoneNumber;
- // 根據phoneNumber在CallerInfoAsyncQuery中開啟具體查詢
- // MTK支援雙SIM卡,是以這裡做了一些修改
- if (info.numberPresentation == Call.PRESENTATION_ALLOWED) {
- // Start the query with the number provided from the call.
- Call c = CallList.getInstance().getCall(call.getCallId());
- boolean isSipPhone = (c == null ? false : (c.getPhoneType() == PhoneConstants.PHONE_TYPE_SIP));
- if (GeminiConstants.SOLT_NUM >= 2 && !isSipPhone) {
- CallerInfoAsyncQuery.startQueryEx(QUERY_TOKEN, context, number,
- listener, call, call.getSlotId());
- } else if (isSipPhone) {
- CallerInfoAsyncQuery.startQueryEx(QUERY_TOKEN, context, number, listener, call, -1);
- } else {
- CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, number,
- listener, call);
- }
- }
- return info;
- }
在以上代碼中,有兩個重要的方法,即buildCallerInfo()和CallerInfoAsyncQuery.startQuery(),先查詢看buildCallerInfo()的關鍵代碼: [java] view plain copy
- public static CallerInfo buildCallerInfo(Context context, CallIdentification identification) {
- CallerInfo info = new CallerInfo();
- // 擷取目前Call的CNAP name
- info.cnapName = identification.getCnapName();
- info.name = info.cnapName;
- info.numberPresentation = identification.getNumberPresentation();
- info.namePresentation = identification.getCnapNamePresentation();
- String number = identification.getNumber();
- // 擷取目前Call的number,如果不為空則執行
- if (!TextUtils.isEmpty(number)) {
- final String[] numbers = number.split("&");
- number = numbers[0];
- if (numbers.length > 1) {
- // info.forwardingNumber = numbers[1];
- }
- // 針對CNAP的情況特殊處理number顯示
- number = modifyForSpecialCnapCases(context, info, number, info.numberPresentation);
- info.phoneNumber = number;
- }
- return info;
- }
如果目前Call的被叫一方沒有開通該業務,則cnapName的值傳回為空。同時,在buildCallerInfo方法中也對目前Call的number是否為空做了判斷。這裡的number來自于網絡側的傳回,比如作為主叫方,當通話接通後被叫方的号碼會通過網絡傳回,在某些特殊的情況下傳回值有可能為空。
關于CNAP
CNAP即Calling Name Presentation的縮寫,是營運商提供的一種服務。比如,使用者開通該服務後,在營運商處設定Calling Name Presentation為"HelloSeven"。當該使用者與其他使用者通話時,如果對方的手機支援CNAP功能,那麼無論對方聯系人裡是否存入了該号碼,displayName都會顯示為"HelloSeven"。加拿大的一些營運商有使用該服務,比如Rogers,但目前國内的營運商均不支援該服務。
As the standard says: “Calling Name Presentation (CNAP) provides the name identification of the calling party (e.g., personal name, company name, “restricted”, “not available”) to the called subscriber”. This means in practice that when somebody calls you your phone will receive not just the number of the callee but also her name information if such is available. More specifically if somebody calls you, you can see her name even if she’s number is not in the contact list stored in your mobile device.
參考1 參考2
查詢資料庫——CallerInfoAsyncQuery
當buildCallerInfo()執行完成後,會根據目前Call的number查詢本機Contacts資料庫。這裡以MTK雙卡為例,是以會執行CallerInfoAsyncQuery.startQueryEx(QUERY_TOKEN, context, number,listener, call, call.getSlotId())方法,關鍵代碼如下( frameworks/base/telephony/java/com/android/internal/telephony/CallerInfoAsyncQuery.java ):
[java] view plain copy
- public static CallerInfoAsyncQuery startQueryEx(int token, Context context, String number,
- OnQueryCompleteListener listener, Object cookie, int simId) {
- //... ...省略
- // 設定SIP Phone的查詢條件
- if (PhoneNumberUtils.isUriNumber(number)) {
- // "number" is really a SIP address.
- contactRef = Data.CONTENT_URI;
- selection = "upper(" + Data.DATA1 + ")=?"
- + " AND "
- + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'";
- selectionArgs = new String[] { number.toUpperCase() };
- } else {
- // 如果是普通号碼,則在PhoneLookup表中查詢
- contactRef = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
- selection = null;
- selectionArgs = null;
- }
- CallerInfoAsyncQuery c = new CallerInfoAsyncQuery();
- c.allocate(context, contactRef);//這裡需要注意,allocate會對mHanlder對象進行指派
- //create cookieWrapper, start query
- CookieWrapper cw = new CookieWrapper();
- cw.listener = listener;
- cw.cookie = cookie;
- cw.number = number;
- cw.simId = simId;
- // ... ...省略
- boolean isECCNumber;
- if (FeatureOption.MTK_GEMINI_SUPPORT) {
- int phoneType = PHONE_TYPE_GSM;
- try {
- ITelephony iTel = ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE));
- phoneType = iTel.getActivePhoneTypeGemini(simId);
- } catch (Exception e) {
- }
- isECCNumber = PhoneNumberUtils.isEmergencyNumberExt(number, phoneType);
- }else {
- isECCNumber = PhoneNumberUtils.isEmergencyNumber(number);
- }
- // 設定查詢類型包括:EMERGENCY_NUMBER、VOICEMAIL、NEW_QUERY
- if (isECCNumber) {
- cw.event = EVENT_EMERGENCY_NUMBER;
- } else if ((originalSimId != -1) && (mPhoneNumberExt.isVoiceMailNumber(number, simId))) {
- cw.event = EVENT_VOICEMAIL_NUMBER;
- } else {
- cw.event = EVENT_NEW_QUERY;
- }
- // 開始查詢
- c.mHandler.startQuery(token,
- cw, // cookie
- contactRef, // uri
- null, // projection
- selection, // selection
- selectionArgs, // selectionArgs
- null); // orderBy
- return c;
- }
以上代碼中主要完成:設定查詢對應的資料庫表;設定查詢類型(緊急号碼、語音号碼、普通查詢);發起資料庫查詢。其中c.allocate()方法會對mHandler進行指派,關鍵代碼如下:
[java] view plain copy
- private void allocate(Context context, Uri contactRef) {
- //... ...省略
- //其中CallerInfoAsyncQueryHandler extends AsyncQueryHandler,是CallerInfoAsyncQuery的内部類
- mHandler = new CallerInfoAsyncQueryHandler(context);
- mHandler.mQueryContext = context;
- mHandler.mQueryUri = contactRef;
- }
當執行c.mHandler.startQuery的時候,會先查詢CallerInfoAsyncQuery.CallerInfoAsyncQueryHandler中是否有startQuery方法,之後再跳轉到父類AsyncQueryHandler的startQuery方法中( frameworks/base/core/java/android/content/AsyncQueryHandler.java ):
[java] view plain copy
- public void startQuery(int token, Object cookie, Uri uri,
- String[] projection, String selection, String[] selectionArgs,
- String orderBy) {
- Message msg = mWorkerThreadHandler.obtainMessage(token);
- //類型為EVENT_ARG_QUERY
- msg.arg1 = EVENT_ARG_QUERY;
- WorkerArgs args = new WorkerArgs();
- //從CallerInfoAsyncQuery.CallerInfoAsyncQueryHandler跳轉到AsyncQueryHandler,是以這裡的this是CallerInfoAsyncQuery.CallerInfoAsyncQueryHandler對象
- args.handler = this;
- args.uri = uri;
- args.projection = projection;
- args.selection = selection;
- args.selectionArgs = selectionArgs;
- args.orderBy = orderBy;
- args.cookie = cookie;
- msg.obj = args;
- //這裡的mWorkerThreadHandler的執行個體在
- mWorkerThreadHandler.sendMessage(msg);
- }
以上代碼有幾個需要注意的地方:
①. args.handler = this
因為代碼是從CallerInfoAsyncQuery.CallerInfoAsyncQueryHandler調用到AsyncQueryHandler.startQuery方法的,是以這裡的this對象是CallerInfoAsyncQuery.CallerInfoAsyncQueryHandler而不是AsyncQueryHandler。
②. mWorkThreadHandler實際上是CallerInfoAsyncQuery.CallerInfoAsyncQueryHandler的對象
通過圖2和圖3可以看到mWorkThreadHandler的執行個體化流程:
圖 2 CallerInfoAsyncQueryHandler以及AsyncQueryHandler關系類圖
圖 3 mWorkThreadHandler執行個體化流程
最後通過mWorkerThreadHandler.sendMessage()方法跳轉到CallerInfoAsyncQuery.CallerInfoAsyncQueryHandler.CallerInfoWorkerHandler的handleMessage方法中,關鍵代碼如下:
[java] view plain copy
- protected class CallerInfoWorkerHandler extends WorkerHandler {
- public CallerInfoWorkerHandler(Looper looper) {
- super(looper);
- }
- @Override
- public void handleMessage(Message msg) {
- WorkerArgs args = (WorkerArgs) msg.obj;
- CookieWrapper cw = (CookieWrapper) args.cookie;
- //這裡的cw在startQueryEx中進行了設定,不為null
- if (cw == null) {
- super.handleMessage(msg);
- } else {
- switch (cw.event) {
- //此時event為NEW_QUERY
- case EVENT_NEW_QUERY:
- //直接回調父類中的handleMessage方法
- super.handleMessage(msg);
- break;
- // shortcuts to avoid query for recognized numbers.
- case EVENT_EMERGENCY_NUMBER:
- case EVENT_VOICEMAIL_NUMBER:
- case EVENT_ADD_LISTENER:
- case EVENT_END_OF_QUEUE:
- // query was already completed, so just send the reply.
- // passing the original token value back to the caller
- // on top of the event values in arg1.
- Message reply = args.handler.obtainMessage(msg.what);
- reply.obj = args;
- reply.arg1 = msg.arg1;
- reply.sendToTarget();
- break;
- default:
- }
- }
- }
- }
如果隻是普通的号碼查詢,則執行case EVENT_NEW_QUERY,回調到父類AsyncQueryHandler.WorkerHandler的handleMessage方法中:
[java] view plain copy
- protected class WorkerHandler extends Handler {
- //... ...省略
- @Override
- public void handleMessage(Message msg) {
- //... ...省略
- switch (event) {
- case EVENT_ARG_QUERY:
- Cursor cursor;
- try {
- // 查詢Contacts資料庫中的PhoneLookup表
- cursorcursor = resolver.query(args.uri, args.projection,
- args.selection, args.selectionArgs,
- args.orderBy);
- // Calling getCount() causes the cursor window to be filled,
- // which will make the first access on the main thread a lot faster.
- if (cursor != null) {
- cursor.getCount();
- }
- } catch (Exception e) {
- Log.w(TAG, "Exception thrown during handling EVENT_ARG_QUERY", e);
- cursor = null;
- }
- // 将查詢結果儲存到args中
- args.result = cursor;
- break;
- case EVENT_ARG_INSERT:
- args.result = resolver.insert(args.uri, args.values);
- break;
- case EVENT_ARG_UPDATE:
- args.result = resolver.update(args.uri, args.values, args.selection,
- args.selectionArgs);
- break;
- case EVENT_ARG_DELETE:
- args.result = resolver.delete(args.uri, args.selection, args.selectionArgs);
- break;
- }
- //注意:這裡的args.handler對象實際上是CallerInfoAsyncQuery.CallerInfoAsyncQueryHandler的執行個體
- Message reply = args.handler.obtainMessage(token);
- reply.obj = args;
- reply.arg1 = msg.arg1;
- reply.sendToTarget();
- }
- }
在WorkHandler中查詢完畢之後,執行args.handler.obtainMessage(),這裡的args.handler實際上是CallerInfoAsyncQuery.CallerInfoAsyncQueryHandler的執行個體,但在CallerInfoAsyncQuery.CallerInfoAsyncQueryHandler中并沒有handleMessage方法,是以回調其父類AsyncQueryHandler的handleMessage方法:
[java] view plain copy
- @Override
- public void handleMessage(Message msg) {
- //... ...省略
- switch (event) {
- // 查詢完畢之後執行
- case EVENT_ARG_QUERY:
- onQueryComplete(token, args.cookie, (Cursor) args.result);
- break;
- case EVENT_ARG_INSERT:
- onInsertComplete(token, args.cookie, (Uri) args.result);
- break;
- case EVENT_ARG_UPDATE:
- onUpdateComplete(token, args.cookie, (Integer) args.result);
- break;
- case EVENT_ARG_DELETE:
- onDeleteComplete(token, args.cookie, (Integer) args.result);
- break;
- }
- }
onQueryComplete方法可以看做this.onQueryComplete,而this來源于CallerInfoAsyncQuery.CallerInfoAsyncQueryHandler。是以,這裡會回調到CallerInfoAsyncQuery.CallerInfoAsyncQueryHandler的onQueryComplete方法中,關鍵代碼如下:
[java] view plain copy
- @Override
- protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
- CookieWrapper cw = (CookieWrapper) cookie;
- // ... ...省略
- //查詢完畢,将查詢結果傳回.
- if (cw.listener != null) {
- cw.listener.onQueryComplete(token, cw.cookie, mCallerInfo);
- }
- // ... ...省略
- }
以上代碼中的cw.listener來自于CallerInfoAsyncQuery.startQueryEx(),在ContactInfoCache.findInfo()中可以看到,listener實際為ContactInfoCache.FindInfoCallback的對象: [java] view plain copy
- final CallerInfo callerInfo = CallerInfoUtils.getCallerInfoForCall(
- mContext, identification, new FindInfoCallback(isIncoming));
- private class FindInfoCallback implements CallerInfoAsyncQuery.OnQueryCompleteListener {
- private final boolean mIsIncoming;
- public FindInfoCallback(boolean isIncoming) {
- mIsIncoming = isIncoming;
- }
- @Override
- public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) {
- final CallIdentification identification = (CallIdentification) cookie;
- findInfoQueryComplete(identification, callerInfo, mIsIncoming, true);
- }
- }
也就是說查詢完成之後會回調到ContactInfoCache.FindInfoCallback的onQueryComplete()方法中,并執行ContactInfoCache.findInfoQueryComplete()。
完善查詢結果——ContactInfoCache
經過各種回調之後,終于将查詢結果傳回到ContactInfoCache.findInfoQueryComplete方法中,該方法主要用于将查詢結果封裝為ContactCacheEntry對象,并發起查詢完畢的回調,關鍵代碼如下:
[java] view plain copy
- private void findInfoQueryComplete(CallIdentification identification,
- CallerInfo callerInfo, boolean isIncoming, boolean didLocalLookup) {
- //将查詢結果儲存到ContactCacheEntry中
- final ContactCacheEntry cacheEntry = buildEntry(mContext, callId,
- callerInfo, presentationMode, isIncoming);
- //将cacheEntry對應與之相對的callId
- mInfoMap.put(callId, cacheEntry);
- if(!mExpiredInfoMap.containsKey(callId)) {
- //将查詢完成的消息通知到相應的回調方法
- sendInfoNotifications(callId, cacheEntry);
- }
- //... ...省略
- }
注意以下兩點:
①. buildEntry()會将最終的顯示内容準備好,以供後續使用;
②. sendInfoNotifications()發起回調,通知相關listener“查詢完畢可供顯示”;
檢視buildEntry()的關鍵代碼如下:
[java] view plain copy
- private ContactCacheEntry buildEntry(Context context, int callId,
- CallerInfo info, int presentation, boolean isIncoming) {
- //... ...省略
- //構造ContatcCacheEntry
- populateCacheEntry(context, info, cce, presentation, isIncoming);
- //... ...省略
- return cce;
- }
在buildEntry方法中,主要是調用populateCacheEntry()完成ContactCacheEntry對象的構造,同時會對緊急号碼做一些處理。populateCacheEntry()關鍵代碼如下: [java] view plain copy
- public static void populateCacheEntry(Context context, CallerInfo info, ContactCacheEntry cce,
- int presentation, boolean isIncoming) {
- Preconditions.checkNotNull(info);
- String displayName = null;
- String displayNumber = null;
- String displayLocation = null;
- String label = null;
- boolean isSipCall = false;
- String number = info.phoneNumber;
- if (!TextUtils.isEmpty(number)) {
- isSipCall = PhoneNumberUtils.isUriNumber(number);
- if (number.startsWith("sip:")) {
- number = number.substring(4);
- }
- }
- // 如果CallerInfo的name為空則執行
- // 通過前面的分析可以知道,CallerInfo的name預設指派為cnapName,而
- // cnapName并不是每個營運商都會支援。是以大多數情況下傳回為空
- if (TextUtils.isEmpty(info.name)) {
- if (TextUtils.isEmpty(number)) {
- // 如果CallerInfo的number也為空則表明目前通話為特殊通話
- // 特殊通話需要顯示Unknown PayPhone Private等特殊字段
- displayName = getPresentationString(context, presentation);
- } else if (presentation != Call.PRESENTATION_ALLOWED) {
- // This case should never happen since the network should never send a phone #
- // AND a restricted presentation. However we leave it here in case of weird
- // network behavior
- displayName = getPresentationString(context, presentation);
- } else if (!TextUtils.isEmpty(info.cnapName)) {
- // 如果cnapName不為空,則将displayName設定未cnapName
- displayName = info.cnapName;
- info.name = info.cnapName;
- displayNumber = number;
- } else {
- // 如果目前通話的号碼并未存儲到使用者的聯系人清單中,将displayNumber設定為
- // 對應的号碼,後面顯示的時候會判斷,如果displayName為空的話,就顯示displayNumber
- displayNumber = number;
- if (isIncoming) {
- // 如果是來電,則顯示号碼歸屬地相關資訊
- displayLocation = info.geoDescription; // may be null
- }
- }
- } else {
- // 如果info.name不為空,則表示之前的cnapName指派成功,則将結果直接顯示
- if (presentation != Call.PRESENTATION_ALLOWED) {
- displayName = getPresentationString(context, presentation);
- } else {
- displayName = info.name;
- displayNumber = number;
- label = info.phoneLabel;
- }
- }
- // 最後将顯示結果存放到ContactCacheEntry對象中
- cce.name = displayName;
- cce.number = displayNumber;
- cce.location = displayLocation;
- cce.label = label;
- cce.isSipCall = isSipCall;
- }
通過以上方法将ContactCacheEntry對象構造完成之後,InCallActivity顯示界面所需要的内容已經準備好,此時會調用sendInfoNotifications()發起回調通知,關鍵代碼如下: [java] view plain copy
- private void sendInfoNotifications(int callId, ContactCacheEntry entry) {
- final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
- if (callBacks != null) {
- for (ContactInfoCacheCallback callBack : callBacks) {
- // 回調所有的onContactInfoComplete方法
- callBack.onContactInfoComplete(callId, entry);
- }
- }
- }
- //在CallCardPresenter的startContactInfoSearch方法裡,發起聯系人查詢時
- //在new ContactInfoCacheCallback()中匿名實作了onContactInfoComplete()
- private void startContactInfoSearch(final CallIdentification identification,
- final int type, boolean isIncoming) {
- cache.findInfo(identification, isIncoming, new ContactInfoCacheCallback() {
- @Override
- public void onContactInfoComplete(int callId, ContactCacheEntry entry) {
- // 更新界面
- updateContactEntry(entry, type, false);
- //... ...省略
- }
- @Override
- public void onImageLoadComplete(int callId, ContactCacheEntry entry) {
- //... ...省略
- }
- });
- }
- //在ContactInfoCache的findInfo方法中添加ContactInfoCacheCallback
- public void findInfo(final CallIdentification identification, final boolean isIncoming,
- ContactInfoCacheCallback callback) {
- //... ...省略
- callBacks.add(callback);//添加callback
- mCallBacks.put(callId, callBacks);
- //... ...省略
- }
當資料庫查詢結束後,最終會通過CallCardPresenter.updateContactEntry()方法來更新界面,關鍵代碼如下: [java] view plain copy
- private void updateContactEntry(ContactCacheEntry entry, int type, boolean isConference) {
- if (type == PRIMARY) {
- mPrimaryContactInfo = entry;
- updatePrimaryDisplayInfo(entry, isConference);
- } else if (type == SECONDARY) {
- mSecondaryContactInfo = entry;
- updateSecondaryDisplayInfo(isConference);
- } else if (type == SECONDARY_ONHOLD) {
- updateSecondaryHoldCallDisplayInfo(isConference);
- } else if (type == SECONDARY_INCOMING) {
- updateSecondaryIncomingCallDisplayInfo(isConference);
- }
- }
界面顯示Unknown的原因
在前面的分析中已經提到,在特殊情況下會顯示特殊的displayName:
[java] view plain copy
- displayName = getPresentationString(context, presentation);
- private static String getPresentationString(Context context, int presentation) {
- String name = context.getString(R.string.unknown);//Unknown 位置号碼
- if (presentation == Call.PRESENTATION_RESTRICTED) {
- name = context.getString(R.string.private_num);// Private 私人号碼
- } else if (presentation == Call.PRESENTATION_PAYPHONE) {
- name = context.getString(R.string.payphone); //Pay Phone 共用電話
- }
- return name;
- }
這裡所說的特殊情況一般指的是營運商提供的一些服務,比如COLP 即Connected Line identification Presentation。該服務國内營運商稱為——号碼隐藏服務,即當使用者開通該業務後,網絡側傳回資料中不會包含該使用者的号碼資訊。該服務目前國内營運商均已不再受理,以前辦理過該業務的号碼持續有效。
比如一名使用者開啟了該服務,呼叫該使用者,當該使用者接通來電後,主叫裝置上不會顯示對方的号碼或者聯系人資訊,取而代之的是Unknown( 未知号碼 )。如果遇到這種情況,可以通過檢視相應的AT日志以及Modem日志來分析(注:MTK使用使用的AT Command,QCom使用的ShareMemory與Modem通信),如圖4:
圖 4 被叫開啟COLP服務,主叫MO AT傳回
關于AT +ECPI指令說明請參看《Android 4.4 Kitkat Phone工作流程淺析(六)__InCallActivity顯示更新流程》。這裡可以看到當被叫接通後,即+ECPI狀态從2->132->6時,Modem側沒有将被叫的号碼傳回。同時,檢視Modem日志資訊也反映出被叫開啟了COLP服務,如圖5:
圖 5 Modem傳回日志
總結
關于InCallUI中displayName的擷取需要注意以下三點:
1. 發起點在CallCardPresenter的init方法中,通過startContactInfoSearch()方法開始查詢;
2. 查詢過程主要分為四步:
①. CallerInfo擷取
在CallerInfoUtils.getCallerInfoForCall()方法中擷取CallerInfo對象。
②. 聯系人資料庫查詢
在CallerInfoUtils.getCallerInfoForCall()方法中調用CallerInfoAsyncQuery.startQueryEx開啟聯系人資料庫查詢。注意:因為MTK在原生AOSP的基礎上修改了代碼,用以支援雙SIM卡,是以有些地方與原生AOSP有些許不同。這裡MTK的代碼會執行frameworks/base/telephony/java/com/android/internal/telephony/CallerInfoAsyncQuery.java中的startQueryEx,而原生AOSP的代碼則會執行packages/app/InCallUI/src/com/android/incallui/CallerInfoAsyncQuery.java中的startQuery。
③. 将查詢結果傳回
聯系人資料庫查詢完畢之後需要将查詢結果傳回,并最終回調到ContactInfoCache.FindInfoCallback的onQueryComplete()方法中。
④. 顯示displayName
最後通過ContactInfoCache.sendInfoNotifications()方式回調到CallCardPresenter中,并更新界面displayName。
3. 界面顯示Unknown的原因,是因為号碼為特殊号碼,displayName的特殊号碼包括:Unknown( 未知号碼 )、Private( 私人号碼 )、Pay Phone( 共用電話 )。具體原因則有可能是網絡傳回異常或營運商特殊服務(COLP/CNAP)等。
整個displayName擷取并顯示流程如圖6所示:
圖 6 displayName查詢顯示流程