本文代码以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,关键代码如下:
- public void init(Context context, Call call) {
-
- if (call != null) {
- mPrimary = call;
- final CallIdentification identification = call.getIdentification();
-
- if (!call.isConferenceCall()) {
-
- startContactInfoSearch(identification, PRIMARY,
- call.getState() == Call.State.INCOMING);
- } else {
-
- updateContactEntry(null, PRIMARY, true);
- }
- }
- }
startContactInfoSearch的具体代码如下:
- private void startContactInfoSearch(final CallIdentification identification,
- final boolean isPrimary, boolean isIncoming) {
- final int type, boolean isIncoming) {
- final ContactInfoCache cache = ContactInfoCache.getInstance(mContext);
-
- cache.findInfo(identification, isIncoming, new ContactInfoCacheCallback() {
- @Override
- public void onContactInfoComplete(int callId, ContactCacheEntry entry) {
-
- 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关键代码如下:
- public void findInfo(final CallIdentification identification, final boolean isIncoming,
- ContactInfoCacheCallback callback) {
-
-
- 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去数据库中查询,关键代码如下:
- public static CallerInfo getCallerInfoForCall(Context context, CallIdentification call,
- CallerInfoAsyncQuery.OnQueryCompleteListener listener) {
-
- CallerInfo info = buildCallerInfo(context, call);
- String number = info.phoneNumber;
-
-
-
- if (info.numberPresentation == Call.PRESENTATION_ALLOWED) {
-
- 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()的关键代码:
- public static CallerInfo buildCallerInfo(Context context, CallIdentification identification) {
- CallerInfo info = new CallerInfo();
-
-
- info.cnapName = identification.getCnapName();
- info.name = info.cnapName;
- info.numberPresentation = identification.getNumberPresentation();
- info.namePresentation = identification.getCnapNamePresentation();
-
- String number = identification.getNumber();
-
- if (!TextUtils.isEmpty(number)) {
- final String[] numbers = number.split("&");
- number = numbers[0];
- if (numbers.length > 1) {
-
- }
-
- 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 ):
- public static CallerInfoAsyncQuery startQueryEx(int token, Context context, String number,
- OnQueryCompleteListener listener, Object cookie, int simId) {
-
-
- if (PhoneNumberUtils.isUriNumber(number)) {
-
- contactRef = Data.CONTENT_URI;
- selection = "upper(" + Data.DATA1 + ")=?"
- + " AND "
- + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'";
- selectionArgs = new String[] { number.toUpperCase() };
- } else {
-
- contactRef = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
- selection = null;
- selectionArgs = null;
- }
-
- CallerInfoAsyncQuery c = new CallerInfoAsyncQuery();
- c.allocate(context, contactRef);
-
-
- 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);
- }
-
- 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,
- contactRef,
- null,
- selection,
- selectionArgs,
- null);
- return c;
- }
以上代码中主要完成:设置查询对应的数据库表;设置查询类型(紧急号码、语音号码、普通查询);发起数据库查询。其中c.allocate()方法会对mHandler进行赋值,关键代码如下:
- private void allocate(Context context, Uri contactRef) {
-
-
- 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 ):
- public void startQuery(int token, Object cookie, Uri uri,
- String[] projection, String selection, String[] selectionArgs,
- String orderBy) {
- Message msg = mWorkerThreadHandler.obtainMessage(token);
-
- msg.arg1 = EVENT_ARG_QUERY;
- WorkerArgs args = new WorkerArgs();
-
- 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.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方法中,关键代码如下:
- 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;
-
- if (cw == null) {
- super.handleMessage(msg);
- } else {
- switch (cw.event) {
-
- case EVENT_NEW_QUERY:
-
- super.handleMessage(msg);
- break;
-
- case EVENT_EMERGENCY_NUMBER:
- case EVENT_VOICEMAIL_NUMBER:
-
- case EVENT_ADD_LISTENER:
- case EVENT_END_OF_QUEUE:
-
-
-
- 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方法中:
- protected class WorkerHandler extends Handler {
-
- @Override
- public void handleMessage(Message msg) {
-
- switch (event) {
- case EVENT_ARG_QUERY:
- Cursor cursor;
- try {
-
- cursorcursor = resolver.query(args.uri, args.projection,
- args.selection, args.selectionArgs,
- args.orderBy);
-
-
- if (cursor != null) {
- cursor.getCount();
- }
- } catch (Exception e) {
- Log.w(TAG, "Exception thrown during handling EVENT_ARG_QUERY", e);
- cursor = null;
- }
-
- 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;
- }
-
- 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方法:
- @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方法中,关键代码如下:
- @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的对象:
- 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对象,并发起查询完毕的回调,关键代码如下:
- private void findInfoQueryComplete(CallIdentification identification,
- CallerInfo callerInfo, boolean isIncoming, boolean didLocalLookup) {
-
- final ContactCacheEntry cacheEntry = buildEntry(mContext, callId,
- callerInfo, presentationMode, isIncoming);
-
- mInfoMap.put(callId, cacheEntry);
- if(!mExpiredInfoMap.containsKey(callId)) {
-
- sendInfoNotifications(callId, cacheEntry);
- }
-
- }
注意以下两点:
①. buildEntry()会将最终的显示内容准备好,以供后续使用;
②. sendInfoNotifications()发起回调,通知相关listener“查询完毕可供显示”;
查看buildEntry()的关键代码如下:
- private ContactCacheEntry buildEntry(Context context, int callId,
- CallerInfo info, int presentation, boolean isIncoming) {
-
-
- populateCacheEntry(context, info, cce, presentation, isIncoming);
-
- 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);
- }
- }
-
-
-
- if (TextUtils.isEmpty(info.name)) {
- if (TextUtils.isEmpty(number)) {
-
-
- displayName = getPresentationString(context, presentation);
- } else if (presentation != Call.PRESENTATION_ALLOWED) {
-
-
-
- displayName = getPresentationString(context, presentation);
- } else if (!TextUtils.isEmpty(info.cnapName)) {
-
- displayName = info.cnapName;
- info.name = info.cnapName;
- displayNumber = number;
- } else {
-
-
- displayNumber = number;
- if (isIncoming) {
-
- displayLocation = info.geoDescription;
- }
- }
- } else {
-
- if (presentation != Call.PRESENTATION_ALLOWED) {
- displayName = getPresentationString(context, presentation);
- } else {
- displayName = info.name;
- displayNumber = number;
- label = info.phoneLabel;
- }
- }
-
- cce.name = displayName;
- cce.number = displayNumber;
- cce.location = displayLocation;
- cce.label = label;
- cce.isSipCall = isSipCall;
- }
通过以上方法将ContactCacheEntry对象构造完成之后,InCallActivity显示界面所需要的内容已经准备好,此时会调用sendInfoNotifications()发起回调通知,关键代码如下:
- private void sendInfoNotifications(int callId, ContactCacheEntry entry) {
- final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
- if (callBacks != null) {
- for (ContactInfoCacheCallback callBack : callBacks) {
-
- callBack.onContactInfoComplete(callId, entry);
- }
- }
- }
-
-
-
- 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) {
-
- }
- });
- }
-
-
- public void findInfo(final CallIdentification identification, final boolean isIncoming,
- ContactInfoCacheCallback callback) {
-
- callBacks.add(callback);
- mCallBacks.put(callId, callBacks);
-
- }
当数据库查询结束后,最终会通过CallCardPresenter.updateContactEntry()方法来更新界面,关键代码如下:
- 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:
- displayName = getPresentationString(context, presentation);
- private static String getPresentationString(Context context, int presentation) {
- String name = context.getString(R.string.unknown);
- if (presentation == Call.PRESENTATION_RESTRICTED) {
- name = context.getString(R.string.private_num);
- } else if (presentation == Call.PRESENTATION_PAYPHONE) {
- name = context.getString(R.string.payphone);
- }
- 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查询显示流程