无论是在MT (Mobile Termination Call被叫——来电),还是MO (Mobile Origination Call主叫——去电) 流程中,通话界面上都会显示当前通话的名称( 后文以displayName指代 )。通常情况下,如果是一个陌生号码,则会显示为该陌生号码。如果是已知联系人,则会显示该联系人的名称。当然,在会议电话( Conference Call )的情况下则直接显示”会议电话”。但是,在某些特殊情况下,displayName还会显示诸如”私人号码”、”公用电话”、”未知号码”等。
本文主要分析displayName的获取显示流程及显示”未知号码”的原因
1、开始查询——CallCardPresenter
displayName是隶属于CallCardFragment的控件,当通话MO/MT流程发起时InCallActivity会显示,此时将会触发CallCardFragment界面更新,在CallCardPresenter的init方法中查询displayName,关键代码如下:
CallCardPresenter.java (\packages\apps\incallui\src\com\android\incallui)
public void init(Context context, Call call) { // Call may be null if disconnect happened already. if (call != null) { mPrimary = call;
// start processing lookups right away. if (!call.isConferenceCall()) { startContactInfoSearch(call, CallEnum.PRIMARY, call.getState() == Call.State.INCOMING);
} else {
/// M: Modified this for MTK DSDA feature. @{
/* Google Code: updateContactEntry(null, true); */
updateContactEntry(null, CallEnum.PRIMARY, true);
/// @}
}
}
}
startContactInfoSearch的具体代码如下:
/** * Starts a query for more contact data for the save primary and secondary calls. */
private void startContactInfoSearch(final Call call, CallEnum type,
boolean isIncoming) {
final ContactInfoCache cache = ContactInfoCache.getInstance(mContext);
// ContactInfoCache中开始查找
cache.findInfo(call, isIncoming, new ContactLookupCallback(this, type));
}
2、异步查询——ContactInfoCache
在CallCardPresenter中发起查询之后会跳转到ContactInfoCache.findInfo()方法中,ContactInfoCache不仅用于查询当前通话的相关信息,还可以将这些信息缓存以备下次查询相同信息时快速返回。findInfo关键代码如下:
/** * Requests contact data for the Call object passed in. * Returns the data through callback. If callback is null, no response is made, however the * query is still performed and cached. * * @param callback The function to call back when the call is found. Can be null. */
public void findInfo(final Call call, final boolean isIncoming,
ContactInfoCacheCallback callback) {
// 查询caller信息,完成之后会回调到FindInfoCallback中,会调用findInfoQueryComplete
final CallerInfo callerInfo = CallerInfoUtils.getCallerInfoForCall(
mContext, call, new FindInfoCallback(isIncoming));
// 当查询完毕之后回调并更新ContactEntry,这里最终会去更新界面显示
findInfoQueryComplete(call, callerInfo, isIncoming, false);
}
CallerInfo中包含了当前call的基本信息,比如号码、类型、特殊相关服务等,在获取到这些信息之后再进行进一步的联系人数据库查询。
3、获取CallerInfo——CallerInfoUtils
在getCallerInfoForCall()方法中,除了获取当前Call的基本信息之外,还会根据当前Call的phoneNumber去数据库中查询,关键代码如下:
ContactInfoCache.java (\packages\apps\incallui\src\com\android\incallui)
/** * This is called to get caller info for a call. This will return a CallerInfo * object immediately based off information in the call, but * more information is returned to the OnQueryCompleteListener (which contains * information about the phone number label, user's name, etc). */
public static CallerInfo getCallerInfoForCall(Context context, Call call,
CallerInfoAsyncQuery.OnQueryCompleteListener listener) {
// 获取当前Call的基本信息并创建CallerInfo对象
CallerInfo info = buildCallerInfo(context, call);
// 根据phoneNumber在CallerInfoAsyncQuery中开启具体查询
if (info.numberPresentation == TelecomManager.PRESENTATION_ALLOWED) {
// Start the query with the number provided from the call.
Log.d(TAG, "==> Actually starting CallerInfoAsyncQuery.startQuery()...");
CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, info, listener, call);
}
return info;
}
在以上代码中,有两个重要的方法,即buildCallerInfo()和CallerInfoAsyncQuery.startQuery(),先查询看buildCallerInfo()的关键代码:
public static CallerInfo buildCallerInfo(Context context, Call call) {
CallerInfo info = new CallerInfo();
// Store CNAP information retrieved from the Connection (we want to do this
// here regardless of whether the number is empty or not).
// 获取当前Call的CNAP name
info.cnapName = call.getCnapName();
info.name = info.cnapName;
info.numberPresentation = call.getNumberPresentation();
info.namePresentation = call.getCnapNamePresentation();
String number = call.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;
}
// Because the InCallUI is immediately launched before the call is connected, occasionally
// a voicemail call will be passed to InCallUI as a "voicemail:" URI without a number.
// This call should still be handled as a voicemail call.
if ((call.getHandle() != null &&
PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme())) ||
isVoiceMailNumber(context, call)) {
info.markAsVoiceMail(context);
}
return info;
}
如果当前Call的被叫一方没有开通该业务,则cnapName的值返回为空。同时,在buildCallerInfo方法中也对当前Call的number是否为空做了判断。这里的number来自于网络侧的返回,比如作为主叫方,当通话接通后被叫方的号码会通过网络返回,在某些特殊的情况下返回值有可能为空。
注:关于CNAP
CNAP即Calling Name Presentation的缩写,是运营商提供的一种服务。比如,用户开通该服务后,在运营商处设置Calling Name Presentation为”HelloSeven”。当该用户与其他用户通话时,如果对方的手机支持CNAP功能,那么无论对方联系人里是否存入了该号码,displayName都会显示为”HelloSeven”。加拿大的一些运营商有使用该服务,比如Rogers,但目前国内的运营商均不支持该服务。
4、查询数据库——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 ):
CallerInfoAsyncQuery.java (\packages\apps\incallui\src\com\android\incallui)
/** * Factory method to start the query based on a CallerInfo object. * * Note: if the number contains an "@" character we treat it * as a SIP address, and look it up directly in the Data table * rather than using the PhoneLookup table. * TODO: But eventually we should expose two separate methods, one for * numbers and one for SIP addresses, and then have * PhoneUtils.startGetCallerInfo() decide which one to call based on * the phone type of the incoming connection. */
public static CallerInfoAsyncQuery startQuery(int token, Context context, CallerInfo info,
OnQueryCompleteListener listener, Object cookie) {
// Construct the URI object and query params, and start the query.
final Uri contactRef = PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI.buildUpon()
.appendPath(info.phoneNumber)
.appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
String.valueOf(PhoneNumberHelper.isUriNumber(info.phoneNumber)))
.build();
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 = info.phoneNumber;
// check to see if these are recognized numbers, and use shortcuts if we can.
// 设置查询类型包括:EMERGENCY_NUMBER、VOICEMAIL、NEW_QUERY
if (PhoneNumberUtils.isLocalEmergencyNumber(context, info.phoneNumber)) {
cw.event = EVENT_EMERGENCY_NUMBER;
} else if (info.isVoiceMailNumber()) {
cw.event = EVENT_VOICEMAIL_NUMBER;
} else {
cw.event = EVENT_NEW_QUERY;
}
// 开始查询
c.mHandler.startQuery(token,
cw, // cookie
contactRef, // uri
null, // projection
null, // selection
null, // selectionArgs
null); // orderBy
return c;
}
以上代码中主要完成:设置查询对应的数据库表;设置查询类型(紧急号码、语音号码、普通查询);发起数据库查询。其中c.allocate()方法会对mHandler进行赋值,关键代码如下:
/** * Method to create a new CallerInfoAsyncQueryHandler object, ensuring correct * state of context and uri. */
private void allocate(Context context, Uri contactRef) {
if ((context == null) || (contactRef == null)){
throw new QueryPoolException("Bad context or query uri.");
}
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 ):
/**
* This method begins an asynchronous query. When the query is done
* {@link #onQueryComplete} is called.
*
* @param token A token passed into {@link #onQueryComplete} to identify
* the query.
* @param cookie An object that gets passed into {@link #onQueryComplete}
* @param uri The URI, using the content:// scheme, for the content to
* retrieve.
* @param projection A list of which columns to return. Passing null will
* return all columns, which is discouraged to prevent reading data
* from storage that isn't going to be used.
* @param selection A filter declaring which rows to return, formatted as an
* SQL WHERE clause (excluding the WHERE itself). Passing null will
* return all rows for the given URI.
* @param selectionArgs You may include ?s in selection, which will be
* replaced by the values from selectionArgs, in the order that they
* appear in the selection. The values will be bound as Strings.
* @param orderBy How to order the rows, formatted as an SQL ORDER BY
* clause (excluding the ORDER BY itself). Passing null will use the
* default sort order, which may be unordered.
*/
public void startQuery(int token, Object cookie, Uri uri,
String[] projection, String selection, String[] selectionArgs,
String orderBy) {
// Use the token as what so cancelOperations works properly
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);
}
最后通过mWorkerThreadHandler.sendMessage()方法跳转到CallerInfoAsyncQuery.CallerInfoAsyncQueryHandler.CallerInfoWorkerHandler的handleMessage方法中,关键代码如下:
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) {
// Normally, this should never be the case for calls originating
// from within this code.
// However, if there is any code that this Handler calls (such as in
// super.handleMessage) that DOES place unexpected messages on the
// queue, then we need pass these messages on.
Log.d(this, "Unexpected command (CookieWrapper is null): " + msg.what +
" ignored by CallerInfoWorkerHandler, passing onto parent.");
super.handleMessage(msg);
} else {
Log.d(this, "Processing event: " + cw.event + " token (arg1): " + msg.arg1 +
" command: " + msg.what + " query URI: " +
sanitizeUriToString(args.uri));
switch (cw.event) {
//此时event为NEW_QUERY
case EVENT_NEW_QUERY:
//start the sql command.
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方法中:
AsyncQueryHandler.java (alps\frameworks\base\core\java\android\content)
protected class WorkerHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (event) {
case EVENT_ARG_QUERY:
Cursor cursor;
try {
// 查询Contacts数据库中的PhoneLookup表
cursor = 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;
}
// passing the original token value back to the caller
// on top of the event values in arg1.
//注意:这里的args.handler对象实际上是CallerInfoAsyncQuery.CallerInfoAsyncQueryHandler的实例
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
if (localLOGV) {
Log.d(TAG, "WorkerHandler.handleMsg: msg.arg1=" + msg.arg1
+ ", reply.what=" + reply.what);
}
reply.sendToTarget();
}
}
在WorkHandler中查询完毕之后,执行args.handler.obtainMessage(),这里的args.handler实际上是CallerInfoAsyncQuery.CallerInfoAsyncQueryHandler的实例,但在CallerInfoAsyncQuery.CallerInfoAsyncQueryHandler中并没有handleMessage方法,因此回调其父类AsyncQueryHandler的handleMessage方法:
@Override
public void handleMessage(Message msg) {
// pass token back to caller on each callback.
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方法中,关键代码如下:
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
try {
//get the cookie and notify the listener.
CookieWrapper cw = (CookieWrapper) cookie;
//notify the listener that the query is complete.
//查询完毕,将查询结果返回.
if (cw.listener != null) {
Log.d(this, "notifying listener: " + cw.listener.getClass().toString() +
" for token: " + token + mCallerInfo);
cw.listener.onQueryComplete(token, cw.cookie, mCallerInfo);
}
} finally {
// The cursor may have been closed in CallerInfo.getCallerInfo()
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
}
}
}
以上代码中的cw.listener来自于CallerInfoAsyncQuery.startQueryEx(),在ContactInfoCache.findInfo()中可以看到,listener实际为ContactInfoCache.FindInfoCallback的对象:
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()。
5、完善查询结果——ContactInfoCache
经过各种回调之后,终于将查询结果返回到ContactInfoCache.findInfoQueryComplete方法中,该方法主要用于将查询结果封装为ContactCacheEntry对象,并发起查询完毕的回调,关键代码如下:
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()的关键代码如下:
private ContactCacheEntry buildEntry(Context context, String callId,
CallerInfo info, int presentation, boolean isIncoming) {
// The actual strings we're going to display onscreen:
Drawable photo = null;
//构造ContatcCacheEntry
final ContactCacheEntry cce = new ContactCacheEntry();
populateCacheEntry(context, info, cce, presentation, isIncoming);
// This will only be true for emergency numbers
if (info.photoResource != 0) {
photo = context.getResources().getDrawable(info.photoResource);
} else if (info.isCachedPhotoCurrent) {
if (info.cachedPhoto != null) {
photo = info.cachedPhoto;
} else {
photo = context.getResources().getDrawable(R.drawable.picture_unknown);
photo.setAutoMirrored(true);
}
} else if (info.contactDisplayPhotoUri == null) {
photo = context.getResources().getDrawable(R.drawable.picture_unknown);
photo.setAutoMirrored(true);
} else {
cce.displayPhotoUri = info.contactDisplayPhotoUri;
}
//mod-start by depeng.li for bug 39488 on 2015.12.23
//if (info.lookupKeyOrNull == null || info.contactIdOrZero == 0) {
// Log.v(TAG, "lookup key is null or contact ID is 0. Don't create a lookup uri.");
// cce.lookupUri = null;
//} else {
cce.lookupUri = Contacts.getLookupUri(info.contactIdOrZero, info.lookupKeyOrNull);
//}
//mod-end by depeng.li for bug 39488 on 2015.12.23
cce.photo = photo;
cce.lookupKey = info.lookupKeyOrNull;
return cce;
}
在buildEntry方法中,主要是调用populateCacheEntry()完成ContactCacheEntry对象的构造,同时会对紧急号码做一些处理。populateCacheEntry()关键代码如下:
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()发起回调通知,关键代码如下:
ContactInfoCache.java (\packages\apps\incallui\src\com\android\incallui)
/** * Sends the updated information to call the callbacks for the entry. */
private void sendInfoNotifications(String callId, ContactCacheEntry entry) {
final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
Log.d(TAG, "onContactInfoComplete sendInfoNotifications()...");
if (callBacks != null) {
for (ContactInfoCacheCallback callBack : callBacks) {
// 回调所有的onContactInfoComplete方法
callBack.onContactInfoComplete(callId, entry);
}
}
}
CallCardPresenter.java (\packages\apps\incallui\src\com\android\incallui)
/** * Starts a query for more contact data for the save primary and secondary calls. */
//在CallCardPresenter的startContactInfoSearch方法里,发起联系人查询时
//在new ContactInfoCacheCallback()中匿名实现了onContactInfoComplete()
private void startContactInfoSearch(final Call call, CallEnum type,
boolean isIncoming) {
final ContactInfoCache cache = ContactInfoCache.getInstance(mContext);
//在ContactInfoCache的findInfo方法中添加ContactInfoCacheCallback
cache.findInfo(call, isIncoming, new ContactLookupCallback(this, type));
}
当数据库查询结束后,最终会通过CallCardPresenter.updateContactEntry()方法来更新界面,关键代码如下:
/** * Update the contact entry and view with specified view type. * * @param entry * @param type Includes the following three types: PRIMARY/SECONDARY/THIRD. * @param isConference */
// 显示的信息是从ContactCacheEntry 获取的
private void updateContactEntry(ContactCacheEntry entry, CallEnum type, boolean isConference) {
switch (type) {
case PRIMARY:
mPrimaryContactInfo = entry;
updatePrimaryDisplayInfo();
break;
case SECONDARY:
mSecondaryContactInfo = entry;
updateSecondaryDisplayInfo();
break;
case THIRD:
mThirdContactInfo = entry;
updateThirdDisplayInfo(isConference);
break;
default:
break;
}
}
三、界面显示Unknown的原因
在前面的分析中已经提到,在特殊情况下会显示特殊的displayName:
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:
总结
关于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所示: