Android 6.0 Phone"通话显示"查询流程

一、概要

无论是在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所示:

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值