通话中联系人信息CallerInfo查询

通话中联系人信息查询用到的两个类CallerInfoAsyncQuery和CallerInfo,这两个类都在frameworks/base/telephony下

frameworks/base/telephony/java/com/android/internal/telephony/CallerInfoAsyncQuery.java

frameworks/base/telephony/java/com/android/internal/telephony/CallerInfo.java

CallerInfo

    public String name;//名字
    public String phoneNumber;//号码
    public String normalizedNumber;//E.164标准格式的号码
    public String geoDescription;//地理位置,归属地
    public String cnapName;//接下来的三个值都是cnap相关,并不在数据库中存储。
    public int numberPresentation;//
    public int namePresentation;//
    public boolean contactExists;//联系人是否存在
    public String phoneLabel;//号码标记,例如手机,单位,传真....,依据numberType和numberLabel得来
    public int    numberType;//号码类型的常量值,例如手机是2,在ContactsContract中定义
    public String numberLabel;//numberType值为0(TYPE_CUSTOM)或者19(TYPE_ASSISTANT),phoneLabel实际就是numberLabel
    public int photoResource;//联系人头像资源id,例如紧急号码使用系统内置资源做头像
    public long contactIdOrZero;//联系人数据库中id
    public boolean needUpdate;//标记callerinfo需要更新
    public Uri contactRefUri;//联系人uri
    public String lookupKey;//查找联系人用,使用它比id查找效率高
    public Uri contactDisplayPhotoUri;//联系人头像uri
    public Uri contactRingtoneUri;//铃声uri
    public boolean shouldSendToVoicemail;//标记号码是否直接转到语音邮箱,如果需要的话app层直接挂断处理
    public Drawable cachedPhoto;//联系人头像文件
    public Bitmap cachedPhotoIcon;//联系人头像icon,小一些,用于通知等需要小图片的地方
    public boolean isCachedPhotoCurrent;//cachedPhoto是否被初始化
    private boolean mIsEmergency;//是否是紧急号码
    private boolean mIsVoiceMail; //是否是语音邮箱号码
CNAP百度百科解释: Calling Name Presentation,这种业务是用户在申请这项业务时,可以向业务部门提交一份号码与名字的对照表。在这个号码与名字对照的信息输入移动通信系统后,如果这个用户呼叫其它号码时,就在呼叫接通振铃时,被叫用户的手机或终端上就会显示来话者的名字。
cnapName,numberPresentation,namePresentation比较特殊,不是在数据库中存储的,这个是来电时候网络上报的。numberPresentation和namePresentation的值在TelecomManager定义。cnapName为用户名,传给被叫方显示。
frameworks/base/telecomm/java/android/telecom/TelecomManager.java
  /**
     * Indicates that the address or number of a call is allowed to be displayed for caller ID.
    */
    public static final int PRESENTATION_ALLOWED = 1; //可见

    /**
     * Indicates that the address or number of a call is blocked by the other party.
     */
    public static final int PRESENTATION_RESTRICTED = 2; //不可见

    /**
     * Indicates that the address or number of a call is not specified or known by the carrier.
     */
    public static final int PRESENTATION_UNKNOWN = 3; //未知,作用基本同PRESENTATION_RESTRICTED

    /**
     * Indicates that the address or number of a call belongs to a pay phone.
     */
    public static final int PRESENTATION_PAYPHONE = 4; //公用电话
CallerInfo中重要的方法有两个:
  public static CallerInfo getCallerInfo(Context context, Uri contactRef, Cursor cursor, int subId) 
  package */ CallerInfo markAsEmergency(Context context)
getCallerInfo依据cursor填充callerinfo中的各个成员,markAsEmergency标记名称和头像为系统资源。

CallerInfoAsyncQuery

CallerInfoAsyncQueryHandler

该类是内部类,继承自frameworks/base/core/java/android/content/AsyncQueryHandler.java,负责完成异步查询。
    public AsyncQueryHandler(ContentResolver cr) {
        super();
        mResolver = new WeakReference<ContentResolver>(cr);
        synchronized (AsyncQueryHandler.class) {
            if (sLooper == null) {
                HandlerThread thread = new HandlerThread("AsyncQueryWorker");
                thread.start();

                sLooper = thread.getLooper();
            }
        }
        mWorkerThreadHandler = createHandler(sLooper);
    }
AsyncQueryHandler构造函数中的sLooper的初始化是关键,使用了HandlerThread,这样sLooper就不是主线程队列了,是一个线程队列。
        protected Handler createHandler(Looper looper) {
            return new CallerInfoWorkerHandler(looper);
        }
createHandler被子类重写,
protected class CallerInfoWorkerHandler extends WorkerHandler {
         ...
            @Override
            public void handleMessage(Message msg) {
                    ...
                    switch (cw.event) {
                        case EVENT_NEW_QUERY:
                            //start the sql command.
                            super.handleMessage(msg);
                            break;
                 ...
            }
         ...
}
调用父类的消息处理方法,注意EVENT_NEW_QUERY和EVENT_ARG_QUERY值都是1:
        @Override
        public void handleMessage(Message msg) {
            final ContentResolver resolver = mResolver.get();
            if (resolver == null) return;

            WorkerArgs args = (WorkerArgs) msg.obj;

            int token = msg.what;
            int event = msg.arg1;

            switch (event) {
                case EVENT_ARG_QUERY:
                    ...
                        cursor = resolver.query(args.uri, args.projection,
                                args.selection, args.selectionArgs,
                                args.orderBy);
                    ...

                    args.result = cursor;
                    break;

                    ...
            }

            Message reply = args.handler.obtainMessage(token);
            reply.obj = args;
            reply.arg1 = msg.arg1;

            reply.sendToTarget();
        }
这样查询的代码就运行在线程中。查询完毕回调怎么触发?AsyncQueryHandler继承自Handler,那么WorkHandler是工作线程,自身的消息队列就是用于通知回调的,上述代码中的args.handler就是自身的消息队列:
  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);
        msg.arg1 = EVENT_ARG_QUERY;

        WorkerArgs args = new WorkerArgs();
        args.handler = this;
        ...

        mWorkerThreadHandler.sendMessage(msg);
    }
handler就是查询时候传递进去的this,this的消息处理方法见:
 @Override
    public void handleMessage(Message msg) {
        WorkerArgs args = (WorkerArgs) msg.obj;

        if (localLOGV) {
            Log.d(TAG, "AsyncQueryHandler.handleMessage: msg.what=" + msg.what
                    + ", msg.arg1=" + msg.arg1);
        }

        int token = msg.what;
        int event = msg.arg1;

        // pass token back to caller on each callback.
        switch (event) {
            case EVENT_ARG_QUERY:
                onQueryComplete(token, args.cookie, (Cursor) args.result);
                break;
                ...
        }
    }
消息处理中调用onQueryComplete,这个是CallerInfoAsyncQueryHandler中实现的:
    protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
            ...
            if (cw.event == EVENT_END_OF_QUEUE) { //EVENT_END_OF_QUEUE表示整个查询流程的结束
                release();
                if (cursor != null) {
                    cursor.close();
                }
                return;
            }

            // check the token and if needed, create the callerinfo object.
            if (mCallerInfo == null) {
                ...
                if (cw.event == EVENT_EMERGENCY_NUMBER) {
                    // Note we're setting the phone number here (refer to javadoc
                    // comments at the top of CallerInfo class).
                    mCallerInfo = new CallerInfo().markAsEmergency(mContext); //标记为紧急号码
                } else if (cw.event == EVENT_VOICEMAIL_NUMBER) {
                    mCallerInfo = new CallerInfo().markAsVoiceMail(cw.subId); //标记为语音邮箱号码
                } else {
                    /// M: CC001: CallerInfo OP Plugin @{
                    //According to CallerInfoExt implementation on L, subId is requested for USIM AAS feature.
                    //mCallerInfo = CallerInfo.getCallerInfo(mContext, mQueryUri, cursor);
                    mCallerInfo = CallerInfo.getCallerInfo(mContext, mQueryUri, cursor, cw.subId); //这个就是之前讲的方法,依据cursor填充callerinfo
                    /// @}
                    if (DBG) Rlog.d(LOG_TAG, "==> Got mCallerInfo: " + mCallerInfo);

                    CallerInfo newCallerInfo = CallerInfo.doSecondaryLookupIfNecessary(
                            mContext, cw.number, mCallerInfo); //sip通话才有可能走这里,一般情况下当这个是空方法
                    if (newCallerInfo != mCallerInfo) {
                        mCallerInfo = newCallerInfo;
                        if (DBG) Rlog.d(LOG_TAG, "#####async contact look up with numeric username"
                                + mCallerInfo);
                    }

                    // Final step: look up the geocoded description.
                    if (ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION) {   //更新归属地,不过这个默认实现比较粗糙,国内还是用三方归属地查询的多
                        ...
                        mCallerInfo.updateGeoDescription(mContext, cw.number);
                        ...
                    }

                    // Use the number entered by the user for display.
                    if (!TextUtils.isEmpty(cw.number)) {
                        mCallerInfo.phoneNumber = PhoneNumberUtils.formatNumber(cw.number,
                                mCallerInfo.normalizedNumber,
                                CallerInfo.getCurrentCountryIso(mContext));
                    }
                }

                if (DBG) Rlog.d(LOG_TAG, "constructing CallerInfo object for token: " + token);

                //notify that we can clean up the queue after this.
                CookieWrapper endMarker = new CookieWrapper();
                endMarker.event = EVENT_END_OF_QUEUE; //流程结束,这里不直接release的原因是后面还可能有消息要处理,例如EVENT_ADD_LISTENER
                startQuery(token, endMarker, null, null, null, null, null);
            }

            //notify the listener that the query is complete.
            if (cw.listener != null) {
                if (DBG) Rlog.d(LOG_TAG, "notifying listener: " + cw.listener.getClass().toString() +
                             " for token: " + token + mCallerInfo);
                cw.listener.onQueryComplete(token, cw.cookie, mCallerInfo); //通知Listener,触发回调
            }

            if (cursor != null) {
               cursor.close();
            }
        }
    }

查询

查询的方法可以看成有两个,一个是依据contacts uri查询,另外一个是依据number查询,这里分析依据号码查询的方法。
    public static CallerInfoAsyncQuery startQuery(int token, Context context, String number,
            OnQueryCompleteListener listener, Object cookie, int subId) {
     

        final Uri contactRef = PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI.buildUpon()
                .appendPath(number)
                .appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
                        String.valueOf(PhoneNumberUtils.isUriNumber(number)))
                .build(); //依据number生成相应uri

        CallerInfoAsyncQuery c = new CallerInfoAsyncQuery();
        c.allocate(context, contactRef);  //初始化相应数据结构,allocate和查询完毕的release方法对应

        //create cookieWrapper, start query
        CookieWrapper cw = new CookieWrapper(); //生成查询方法中传入的cookie对象
        cw.listener = listener;
        cw.cookie = cookie;
        cw.number = number;
        cw.subId = subId;

        // check to see if these are recognized numbers, and use shortcuts if we can.
        /// M: CC003: Query ECC via EmergencyNumberExt @{
        int phoneType = TelephonyManager.getDefault().getCurrentPhoneType(cw.subId);
        if (PhoneNumberUtils.isEmergencyNumberExt(number, phoneType)) { //依据号码类型传递不同的event值,如果是紧急号码根本就不必去数据库中查询了
        /// @}
            cw.event = EVENT_EMERGENCY_NUMBER;
        } else if (PhoneNumberUtils.isVoiceMailNumber(subId, number)) {
            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;
    }

添加回调

    public void addQueryListener(int token, OnQueryCompleteListener listener, Object cookie) {

        if (DBG) Rlog.d(LOG_TAG, "adding listener to query: " + sanitizeUriToString(mHandler.mQueryUri) +
                " handler: " + mHandler.toString());

        //create cookieWrapper, add query request to end of queue.
        CookieWrapper cw = new CookieWrapper();
        cw.listener = listener;
        cw.cookie = cookie;
        cw.event = EVENT_ADD_LISTENER;

        mHandler.startQuery(token, cw, null, null, null, null, null);
    }
发送EVENT_ADD_LISTENER消息,最终会回到CallerInfoAsyncQueryHandler的onQueryComplete触发一次回调。
这个要对消息队列理解清楚。例如app代码中先是查询,然后添加了2个Listener,那么CallerInfoAsyncQueryHandler的的工作线程要依次处理EVENT_NEW_QUERY、EVENT_ADD_LISTENER、EVENT_ADD_LISTENER三个消息;主线程依次要处理三个EVENT_NEW_QUERY消息,这三个消息都调用了onQueryComplete;对onQueryComplete来说首先处理EVENT_NEW_QUERY消息得到了CallerInfo并往主线程队列投递了EVENT_END_OF_QUEUE消息,然后两次处理EVENT_ADD_LISTENER触发listener的回调,最后处理EVENT_END_OF_QUEUE结束整个流程。
注意onQueryComplete中的这句:
   mCallerInfo = CallerInfo.getCallerInfo(mContext, mQueryUri, cursor, cw.subId);
当EVENT_ADD_LISTENER消息处理走到这的时候cursor是null,但是getCallerInfo在cursor为null的时候不会有任何动作,这个算是一个trick,因为一般理解cursor为null的时候mCallerInfo对应也该置null才对。
从这个流程也看出addQueryListener一定要在startQuery语句后调用。

InCallUI中的联系人查询

packages/apps/InCallUI/src/com/android/incallui/CallerInfo.java
packages/apps/InCallUI/src/com/android/incallui/CallerInfoAsyncQuery.java
mtk的InCallUI代码中也有CallerInfo和CallerInfoAsyncQuery两个类,直接就是拷贝framework中的代码,这个就是做app的为了不修改framework常用的方法,理论上肯定不推荐,但是这也是很多公司代码权限和编译管理太蛋疼的结果。

ContactInfoCache

InCallUI中重要的类,作用是联系人信息的缓存,在UI刷新中多次要获取联系人信息,如果每次都是去数据库查会影响效率。
packages/apps/InCallUI/src/com/android/incallui/ContactInfoCache.java
    public static synchronized ContactInfoCache getInstance(Context mContext) {
        if (sCache == null) {
            sCache = new ContactInfoCache(mContext.getApplicationContext());
        }
        return sCache;
    }
单例模式,方便其它地方调用
  public void findInfo(final Call call, final boolean isIncoming,
            ContactInfoCacheCallback callback) {
        Preconditions.checkState(Looper.getMainLooper().getThread() == Thread.currentThread());
        Preconditions.checkNotNull(callback);

        final String callId = call.getId();
        final ContactCacheEntry cacheEntry = mInfoMap.get(callId); //mInfoMap是缓存
        Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);

        if (cacheEntry != null) {         
            callback.onContactInfoComplete(callId, cacheEntry); //如果缓存有值,直接触发回调并返回
            if (callBacks == null) {
                return;
            }
        }

        // If the entry already exists, add callback
        if (callBacks != null) {  //如果正在查询,添加callback并返回
            callBacks.add(callback);
            return;
        }
     
        callBacks = new CopyOnWriteArraySet<ContactInfoCacheCallback>();     
        callBacks.add(callback);
        mCallBacks.put(callId, callBacks);     
        mCallBackCancel.put(callId, false);
        final CallerInfo callerInfo = CallerInfoUtils.getCallerInfoForCall(
                mContext, call, new FindInfoCallback(isIncoming)); //开始查询

        findInfoQueryComplete(call, callerInfo, isIncoming, false);
    }
其它代码要获取信息的入口,传递的回调定义如下:
    public interface ContactInfoCacheCallback {
        public void onContactInfoComplete(String callId, ContactCacheEntry entry); //信息查询完毕
        public void onImageLoadComplete(String callId, ContactCacheEntry entry);  //头像查询完毕
    }
返回的结构数据结构ContactCacheEntry如下:
public static class ContactCacheEntry {
        public String name;
        public String number;
        public String location;
        public String label;
        public Drawable photo;
        public boolean isSipCall;
        public Uri contactUri;
        public Uri displayPhotoUri;
        public Uri lookupUri; // Sent to NotificationMananger
        public String lookupKey;
        ...
    }
成员看字面意思就可以了解意思了,已经详细介绍过的callerinfo中的成员比这个还多。cnap等都已经处理完毕,这个就是最后要呈现到UI上的元素,这里看出ContactCacheEntry是把CallerInfo又封装了一层。
开始查询的getCallerInfoForCall定义如下:
  public static CallerInfo getCallerInfoForCall(Context context, Call call,
            CallerInfoAsyncQuery.OnQueryCompleteListener listener) {
        CallerInfo info = buildCallerInfo(context, call);
        if (info.numberPresentation == TelecomManager.PRESENTATION_ALLOWED) {
            CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, info, listener, call);
        }
        return info;
    }
调用CallerInfoAsyncQuery发起查询,这个是异步的,回调是:
  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) {        
                  ...
                    findInfoQueryComplete((Call) cookie, callerInfo, mIsIncoming, true);
                  ...
        }
    }

调用findInfoQueryComplete:

     private void findInfoQueryComplete(Call call, CallerInfo callerInfo, boolean isIncoming,
            boolean didLocalLookup) {
            ...
            mInfoMap.put(callId, cacheEntry); //缓存信息
            ...
            sendInfoNotifications(callId, cacheEntry); //触发回调
            ...
                ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
                        mContext, cacheEntry.displayPhotoUri, ContactInfoCache.this, callId); //开始获取头像,这个也是异步的
            ...
    }

缓存信息并发起获取联系人头像的请求,因为数据库表中存储的是头像的uri。
   @Override
    public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
        final String callId = (String) cookie;
        final ContactCacheEntry entry = mInfoMap.get(callId);

        if (entry == null) {
            Log.e(this, "Image Load received for empty search entry.");
            clearCallbacks(callId);
            return;
        }
        Log.d(this, "setting photo for entry: ", entry);

        // Conference call icons are being handled in CallCardPresenter.
        if (photo != null) {
            Log.v(this, "direct drawable: ", photo);
            entry.photo = photo;
        } else if (photoIcon != null) {
            Log.v(this, "photo icon: ", photoIcon);
            entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
        } else {
            Log.v(this, "unknown photo");
            entry.photo = null;
        }

        sendImageNotifications(callId, entry); //触发头像回调
        clearCallbacks(callId);

    }
设置头像并清除所有回调。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现 Android 多人语音通话混音播放 PCM 的一种方法是使用 AudioTrack 类。首先,你需要将每个人的 PCM 数据进行混音,然后将混音后的 PCM 数据传递给 AudioTrack 对象进行播放。 以下是一些实现步骤: 1. 创建一个 AudioTrack 对象,并设置相关参数。 ``` int sampleRateInHz = 44100; int channelConfig = AudioFormat.CHANNEL_OUT_MONO; int audioFormat = AudioFormat.ENCODING_PCM_16BIT; int bufferSizeInBytes = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat); AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_VOICE_CALL, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM); ``` 2. 将每个人的 PCM 数据进行混音。可以通过将每个人的 PCM 数据的每个采样点相加并除以参与混音的人数来实现混音。这里假设有两个人的 PCM 数据。 ``` byte[] pcmData1 = ...; // 第一个人的 PCM 数据 byte[] pcmData2 = ...; // 第二个人的 PCM 数据 byte[] mixPcmData = new byte[pcmData1.length]; for (int i = 0; i < pcmData1.length; i += 2) { // 将每个采样点相加并除以 2 short sample1 = (short) ((pcmData1[i + 1] << 8) | pcmData1[i]); short sample2 = (short) ((pcmData2[i + 1] << 8) | pcmData2[i]); short mixSample = (short) ((sample1 + sample2) / 2); mixPcmData[i] = (byte) (mixSample & 0xff); mixPcmData[i + 1] = (byte) ((mixSample >> 8) & 0xff); } ``` 3. 将混音后的 PCM 数据传递给 AudioTrack 对象进行播放。 ``` audioTrack.play(); audioTrack.write(mixPcmData, 0, mixPcmData.length); ``` 需要注意的是,在多人语音通话,每个人的 PCM 数据的采样率、声道数、采样位数等参数可能不同,需要进行处理以确保混音后的 PCM 数据的参数一致。同时,由于混音后的 PCM 数据可能会比单个人的 PCM 数据更大,需要确保 AudioTrack 对象的缓冲区足够大以避免播放时出现丢失数据的情况。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值