基于5.1代码Contacts模块拨号流程
之前的总结介绍过联系人界面的快速拨号流程以及显示界面的接收数据过程,现在着重讲中间是怎么从OutgoingBroadcast过来拨号界面,中间的联系人信息是如何传递的,以及传递的框架机制。本文通过bug“联系人中多个不同联系人相同号码拨号时如何显示正确姓名”来介绍。
一.Contacts中单击事件
1.Contacts中PeopleActivity中的DefaultContactBrowseListFragment,createListAdapter方法中定位到ContactsCommon模块的DefaultContactListAdapter中,看其父类ContactListAdapter--bindView方法又调用了bindQuickCallView方法(view是由本类的newView方法中调用父类ContactEntryListAdapter的newView方法中的ContactListItemView类实例来的),参考ContactsCommon模块中的ContactListItemView,其中初始化了快速拨号view--R.drawable.ic_action_call,在ContactListAdapter中注册了单击事件View.OnClickListenermClickListener;在监听事件中:
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
new String[] {ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY},
ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY + "=?",newString[] { lookup }, null);
需要增加ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME来获取姓名,再用intent传递出去;
2.在5.1代码上没有了快速拨号按钮,要进联系人信息中然后单击拨号才能打电话,所以在这里给值也发生了变化:
2.1.在QuickContactActivity中,显示电话号码等信息是由populateContactAndAboutCard方法中的(R.id.header):
if(contactCardEntries.size() > 0) {
mContactCard.initialize(contactCardEntries,
/* numInitialVisibleEntries =*/ MIN_NUM_CONTACT_ENTRIES_SHOWN,
/* isExpanded = */mContactCard.isExpanded(),
/* isAlwaysExpanded = */false,
mExpandingEntryCardViewListener,
mScroller);
mContactCard.setVisibility(View.VISIBLE);
} else {
mContactCard.setVisibility(View.GONE);
处理;
2.2.定位到ExpandingEntryCardView的initialize方法-->inflateInitialEntries-->createEntryView:
final TextView header =(TextView) view.findViewById(R.id.header);
if (!TextUtils.isEmpty(entry.getHeader())) {
header.setText(entry.getHeader());
} else {
header.setVisibility(View.GONE);
}
在这里赋值;
2.3.回到QuickContactActivity中,大标题中的姓名是在bindContactData方法中布局是mScroller(mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller)):
setHeaderNameText(ContactDisplayUtils.getDisplayName(this,data).toString());
在这里赋值;
修改显示正确姓名过程:
QuickContactsActivity.java(packages/apps/Contacts)-->mEntryClickHandler:
intent.putExtra("DIS_NAME",ContactDisplayUtils.getDisplayName(QuickContactActivity.this,mContactData).toString());//yyj
二.Sevice中接收Intent与分解过程
1.单击后跳转根据android.intent.action.CALL定位到/packages/service/Telephony中的./src/com/android/phone/OutgoingCallBroadcaster.java,进行intent的判断,最终会执行:
PhoneGlobals.getInstance().callController.placeCall(intent);
上面代码的作用是真正的拨号打电话发出信号过程,走到framework层完成拨号发送命令请求,最后在命令发送成功后会继续执行:
Intent broadcastIntent = newIntent(Intent.ACTION_NEW_OUTGOING_CALL);
这一句的作用在5.1/5.0上做了改动,4.4上面是直接使用startSipCallOptionHandler方法跳转到InCallUI去显示拨号界面然后finish,后面的broadcastIntent没有执行,而5.1/5.0中的startSipCallOptionHandler方法体中是空的,也就是说,直接走到broadcastIntent,发出了广播 ACTION_NEW_OUTGOING_CALL,这个广播可以很容易查出来是定位到了NewOutgoingCallIntentBroadcaster中(packages/services/Telecom)中,这个广播有何作用呢,查看此广播的注释或者processIntent方法注释:
/**
* OutgoingCallIntentBroadcasterreceives CALL and CALL_PRIVILEGED Intents, and broadcasts the
* ACTION_NEW_OUTGOING_CALL intent.ACTION_NEW_OUTGOING_CALL is an ordered broadcast intent which
* contains the phone number beingdialed. Applications can use this intent to (1) see which numbers
* are being dialed, (2) redirect acall (change the number being dialed), or (3) prevent a call
* from being placed.
*@return {@linkCallActivity#OUTGOING_CALL_SUCCEEDED} if the call succeeded, and an appropriate{@link DisconnectCause} if the call did not, describing why it failed.
这句话说明了如果拨号成功最后会来到此广播中(事实上就是上面的intent),由此广播链接到CallActivity(packages/services/Telecom)中,进入此类中可以查看到最后执行了:
intent.setClass(this, CallReceiver.class);
这行代码很明显,最后进入到了 CallReceiver中,
最后发出广播给packages/services/Telecomm/src/com/android/server/telecom/CallReceiver.java,然后调用processOutgoingCallIntent方法,在此方法中最终会调用:
Call call =getCallsManager().startOutgoingCall(handle, phoneAccountHandle, clientExtras);
2.因为传递的是handle,只包含电话号码,此时要想办法在调用startOutgoingCall方法之前加入联系人姓名信息,在这里注意到clientExtras,这个bundle是用来传递值的,正好利用其来put进联系人的姓名:
//yyj add begin--for display correct namein 2-3 the same number
if(intent.getStringExtra("DIS_NAME") != null &&!"".equals(intent.getStringExtra("DIS_NAME"))){
clientExtras.putString("DIS_NAME",intent.getStringExtra("DIS_NAME"));
}
//yyj add end
三.CallsManager完成每路电话的控制管理
1.根据getCallsManager()可以定位到packages/services/Telecomm模块下的CallsManager.java中,进入startOutgoingCall方法,在这里构造了一个Call类的实例,这个Call是packages/services/Telecomm模块下的Call.java类,不过在这里:call.setExtras(extras),对传递进来的 clientExtras进行了一次保存,所以这个call可以暂时不用管,继续在最后调用addCall(call)方法;
进入addCall()方法,在这里,对call进行了保存,保存在一个列表mCalls 中,以待后用,继续:
private void addCall(Call call) {
Log.v(this,"addCall(%s)", call);
call.addListener(this);
mCalls.add(call);
// TODO:Update mForegroundCall prior to invoking
// onCallAddedfor calls which immediately take the foreground (like the first call).
for(CallsManagerListener listener : mListeners) {
listener.onCallAdded(call);
}
updateCallsManagerState();
}
这里 listener是CallsManagerListener类型的,是从mListeners列表中遍历出来的,构造方法中mListeners.add(mInCallController),这个 mInCallController是InCallController类型的,InCallController最终继承了CallsManagerListener,也就是说listener.onCallAdded(call)最终调用了InCallController中实现的 onCallAdded方法来保存call实例;
2.进入packages/services/Telecomm模块下的InCallController.java类中查看是否有onCallAdded方法,果然有此方法的实现:
public void onCallAdded(Call call) {
if(mInCallServices.isEmpty()) {
bind();
} else {
Log.i(this, "onCallAdded: %s", call);
// Track the call if we don't already know about it.
addCall(call);
for (Map.Entry
ComponentName componentName = entry.getKey();
IInCallService inCallService = entry.getValue();
ParcelableCall parcelableCall = toParcelableCall(call,
componentName.equals(mInCallComponentName) /* includeVideoProvider */);
try {
inCallService.addCall(parcelableCall);
} catch (RemoteException ignored) {
}
}}}
在此方法中注意,第一次拨号的时候会调用bind(),看条件mInCallServices.isEmpty(),其实mInCallServices是一个保存IInCallService实例的Map,当这个Map是空的时候就执行bind方法,先进bind方法在这里会进行一些判断和值的保存的工作,继续,很显然,在这里用到了Binder机制,当然也就用到了aidl来绑定service,在bind调用结束后会调用onConnected方法,进入onConnected方法:
private voidonConnected(ComponentName componentName, IBinder service) {
ThreadUtil.checkOnMainThread();
Log.i(this,"onConnected to %s", componentName);
IInCallServiceinCallService = IInCallService.Stub.asInterface(service);
try {
inCallService.setInCallAdapter(new InCallAdapter(CallsManager.getInstance(),
mCallIdMapper));
mInCallServices.put(componentName, inCallService);
} catch(RemoteException e) {
Log.e(this, e, "Failed to set the in-call adapter.");
return;
}
// Uponsuccessful connection, send the state of the world to the service.
ImmutableCollection
if(!calls.isEmpty()) {
Log.i(this, "Adding %s calls to InCallService after onConnected: %s",calls.size(),componentName);
for (Call call : calls) {
try {// Track the call if we don't already know about it.
Log.i(this, "addCall after binding: %s",call);
addCall(call);
inCallService.addCall(toParcelableCall(call,
componentName.equals(mInCallComponentName) /* includeVideoProvider */));
} catch (RemoteException ignored) {
}}
onAudioStateChanged(null, CallsManager.getInstance().getAudioState());
} else {
unbind();
}}
在这里看到 calls,其实是从CallsManager中拿出来的,回看就会看到addCall方法中已经进行过保存了,拿出刚才的call实例,调用:
nCallService.addCall(toParcelableCall(call,componentName.equals(mInCallComponentName));
先看 toParcelableCall方法,是将Call实例转换成了ParcelableCall实例,有什么意义呢,当然有,ParcelableCall继承了Parcelable,持久化数据,这样的话,Call才能被转换成可以使用Handler传递的对象数据,在这里要加上自己的联系人姓名字段并且给值,这样的话,数据就传递到了下一层(framework层);
四.数据通过IInCallService进入到framework完成转换处理
1.上面执行了什么呢?nCallService是什么,看代码,最后发现:
IInCallService inCallService =IInCallService.Stub.asInterface(service);
呵呵,aidl中的IInCallService实例,看上面代码,mInCallServices.put(componentName, inCallService),发现,只要是有继承了这个aidl的类就会被绑定,就保存其实例到列表mInCallServices,最后查明进入到了frameworks/base/telecomm的InCallService.java类中;
2.进入frameworks/base/telecomm的InCallService.java类,恍然大悟,InCallServiceBinder,即被绑定到InCallController中的连接,看看,确实继承了IInCallService.Stub,并且实现了 addCall方法,进入此方法,调用了mHandler,看mHandler实例:
case MSG_SET_IN_CALL_ADAPTER:
mPhone = new Phone(new InCallAdapter((IInCallAdapter) msg.obj));
onPhoneCreated(mPhone);
break;
case MSG_ADD_CALL:
mPhone.internalAddCall((ParcelableCall) msg.obj);
break;
在此case中,会先调用第一个,看不懂没关系,先看onPhoneCreated(mPhone),这个最终会调用子类中的方法,其实是InCallServiceImpl(packages/apps/InCallUI)中的onPhoneCreated,先看:
@Override
public voidonPhoneCreated(Phone phone) {
Log.v(this,"onPhoneCreated");
CallList.getInstance().setPhone(phone);
AudioModeProvider.getInstance().setPhone(phone);
TelecomAdapter.getInstance().setPhone(phone);
InCallPresenter.getInstance().setPhone(phone);
InCallPresenter.getInstance().setUp(
getApplicationContext(),
CallList.getInstance(),
AudioModeProvider.getInstance());
TelecomAdapter.getInstance().setContext(InCallServiceImpl.this);
}
这个方法中会调用CallList.getInstance().setPhone(phone),先不看别的,进入到CallList(packages/apps/InCallUI)中的 setPhone:
public void setPhone(Phone phone) {
mPhone =phone;
mPhone.addListener(mPhoneListener);
}
看 mPhoneListener:
private Phone.Listener mPhoneListener =new Phone.Listener() {
@Override
public voidonCallAdded(Phone phone, android.telecom.Call telecommCall) {
Call call = new Call(telecommCall);
Log.d(this, "onCallAdded: telecommCall name is:" + call.getState());
if (call.getState() == Call.State.INCOMING ||
call.getState() == Call.State.CALL_WAITING) {
onIncoming(call, call.getCannedSmsResponses());
} else {
onUpdate(call);
}}
@Override
public voidonCallRemoved(Phone phone, android.telecom.Call telecommCall) {
if (mCallByTelecommCall.containsKey(telecommCall)) {
Call call = mCallByTelecommCall.get(telecommCall);
call.setState(Call.State.DISCONNECTED);
call.setDisconnectCause(newDisconnectCause(DisconnectCause.UNKNOWN));
if (updateCallInMap(call)) {
Log.w(this, "Removing call not previouslydisconnected " + call.getId());
}
updateCallTextMap(call, null);
}
}
};
主要是onUpdate(call)方法,在此方法追踪下去,这里给 mPhone注册了 mPhoneListener不会马上调用,继续回到上一层InCallService的case中,继续,会调用:
mPhone.internalAddCall((ParcelableCall)msg.obj);//在此方法中会将姓名信息保存到call中:
//yyj add begin--for display correct namein 2-3 the same number
if(parcelableCall.getInCallUiDisName() != null && !"".equals(parcelableCall.getInCallUiDisName())){
call.setDisNameBySameNo(parcelableCall.getInCallUiDisName());
}
//yyj add end
在这里追踪Phone(frameworks\base\telecomm\java\android\telecom)类,可以看到最终会调用到:
private void fireCallAdded(Call call) {
for (Listenerlistener : mListeners) {
listener.onCallAdded(this, call);
}}
追查即知,调用的就是 mPhoneListener的 onCallAdded方法,在这里最终会在CallList的mCallById列表中将此Call加入进去,以便CallCardFragment调用显示到拨号界面,注意,这里的参数call是类型 android.telecom.Call telecommCall,继而调用newCall(telecommCall)进行了转换,转换成了InCallUI模块中的Call类以便调用;
3.让我们来看最后是如何拿到这个call来显示姓名的,在CallCardFragment(packages/apps/InCallUI)中
@Override
public voidonActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final CallListcalls = CallList.getInstance();
final Callcall = calls.getFirstCall();
getPresenter().init(getActivity(), call);
}
继而到了CallCardPresenter中的init中的startContactInfoSearch方法,接着onContactInfoComplete方法中。。最终调用updatePrimaryDisplayInfo,在这里:
//yyj add begin--for display correct namein 2-3 the same number
if(mPrimary.getTelecommCall().getDisNameBySameNo() != null&&!"".equals(mPrimary.getTelecommCall().getDisNameBySameNo())){
checkIdpName = mPrimary.getTelecommCall().getDisNameBySameNo();
}
//yyj add end
我拿出了传进来的call中的姓名信息,显示:
ui.setPrimary(number, checkIdpName,nameIsNumber, mPrimaryContactInfo.label,
mPrimaryContactInfo.photo, isConference,canManageConference,
mPrimaryContactInfo.isSipCall,isForwarded);
checkIdpName即是姓名信息,由setPrimary来更新拨号界面中的联系人信息。
五.剖析代码逻辑存在的问题与解决办法
1.经过上面一系列的讲解,大家可能会以为姓名显示这个功能最终实现了,事实上,并没有,由于android代码中各个层的访问权限不一样,在模块编译的情况下上面的代码没有问题,但是进行整编的时候,android中的脚本对类和类之间的访问做了很强的控制,导致上面的packages/apps层的类访问packages\services和frameworks\base出现问题,哪里出现问题了呢?具体是以下问题:
1.1.ParcelableCall.java(frameworks\base\telecomm\java\android\telecom):我们在此类中添加了字段用以保存姓名,但是最后在CallCardPresenter(packages\apps\incallui)中去显示调用的时候会发现 ParcelableCall已经被声明成了@hide,整编会报错,也就是说如果不在frameworks/base/api/system-current.txt中去声明是访问不到的,当然了被声明成@hide如果强制访问的话是可以的,但是会出现什么不能预期的问题就不得而知的,这是很危险的动作,所以尽量不要去做,但是在InCallController还是访问了这就有点搞不明白了,可能加了特殊控制,所以我们最后也还是要通过ParcelableCall去访问变量,但不是我们自己定义的getDisNameBySameNo等等,因为没有声明所以会出错,我们自己也不要再加,通过查看ParcelableCall类我们发现了一个字段mCallerDisplayName,这个字段在多次打log时发现永远是空的,也就是说没有被用到,而且通过看字面意思似乎也是为了保存姓名信息而存在的,这个时候我们就可以利用了,在InCallController.java(packages\services\telecomm\src\com\android\server\telecom)类中的toParcelableCall方法中我们可以这样:
if(call.getExtras().getString("DIS_NAME")!= null && !"".equals(call.getExtras().getString("DIS_NAME"))){//yyj
callerDisplayName =call.getExtras().getString("DIS_NAME");//yyj
}
其他的不用去管,这样mCallerDisplayName有值了,最后赋值给Call类中的字段即可;
1.2.Call.java(frameworks\base\telecomm\java\android\telecom):同样的,framework层的Call最后会被传入到apps层,之前我们做过一个动作,就是在Phone中给Call赋值以便传入的时候能够拿出姓名信息:call.setDisNameBySameNo(parcelableCall.getInCallUiDisName());----这行代码其实是没有问题的,为什么呢,虽然Call也是被声明成了@hide,但是同一个包中是可以访问的,现在不需要了,因为我发现在Call中的内部类Details中也有一个字段mCallerDisplayName,似乎跟 ParcelableCall如出一辙,事实上确实如此,此字段可以接受 ParcelableCall传过来的值,这个不用手动去改,因为在Phone类的internalAddCall方法中最后调用了:
call.internalUpdate(parcelableCall, mCallByTelecomCallId);
这行代码实际上实例化call的同时会用 parcelableCall去实例化内部类Details,当然也包括给字段mCallerDisplayName赋值,恰好使用的是 ParcelableCall类的mCallerDisplayName,后者已经在上面有值了;
这样的话,整个衔接过程完成了而且省去了中间赋值的过程;
1.3.最后显示姓名信息的时候,依然是在CallCardPresenter.java(packages/apps/incallui)-->init()-->updatePrimaryDisplayInfo(),换了方式:
//yyj add begin--for display correct namein 2-3 the same number
if(mPrimary.getTelecommCall().getDetails().getCallerDisplayName()!= null
&&!"".equals(mPrimary.getTelecommCall().getDetails().getCallerDisplayName())){
name =mPrimary.getTelecommCall().getDetails().getCallerDisplayName();
}
//yyj add end
六.扮演重要角色类的工作原理
上面介绍了整个流程,在这里有必要介绍下Phone和 InCallController:
1.Phone.java(frameworks\base\telecomm\java\android\telecom):
新增加的Phone.java在(frameworks\base\telecomm\java\android\telecom)目录下,其功 能是“A unified virtual device providing a means of voice (and other) communicationon a device.”,即作为一个虚拟设备提供通信服务。其类型如下,
public final class Phone {}
它的方法的实现主要依赖3个辅助类,InCallAdapter、Listener、Call,即呼叫适配器、监听器、控制器;
对于Phone的初始化,我们可以通过其方法的调用关系找到,例如查找internalAddCall,我们就会发现调用者为InCallService,其中mPhone即为Phone的实例,
case MSG_ADD_CALL:
mPhone.internalAddCall((ParcelableCall)msg.obj);
在InCallService里,当收到MSG_SET_IN_CALL_ADAPTER消息时,会创建一个Phone实例,同时也顺带创建了一个InCallAdapter实例,
case MSG_SET_IN_CALL_ADAPTER:
mPhone = new Phone(newInCallAdapter((IInCallAdapter) msg.obj));
onPhoneCreated(mPhone);
break;
MSG_SET_IN_CALL_ADAPTER消息是setInCallAdapter发出的,它是在InCallService里实现的AIDL接口类的服务端方法,
private final class InCallServiceBinderextends IInCallService.Stub {
@Override
public voidsetInCallAdapter(IInCallAdapter inCallAdapter) {
mHandler.obtainMessage(MSG_SET_IN_CALL_ADAPTER,inCallAdapter).sendToTarget();
}
根据我们对service的了解,它在service类创建时创建binder,在service的onbind方法里将binder实例传入,执行客户端的ServiceConnection的onServiceConnected方法,所以在客户端,onConnected被执行,之后就是客户端 InCallController里的setInCallAdapter被调用,再间接调用前面提到的服务端的setInCallAdapter。
private class InCallServiceConnectionimplements ServiceConnection {
/** {@inheritDoc} */
@Override public voidonServiceConnected(ComponentName name, IBinder service) {
Log.d(this, "onServiceConnected:%s", name);
onConnected(name, service);
}
private void onConnected(ComponentNamecomponentName, IBinder service) {
ThreadUtil.checkOnMainThread();
Log.i(this, "onConnectedto %s", componentName);
IInCallService inCallService= IInCallService.Stub.asInterface(service);
try {
inCallService.setInCallAdapter(newInCallAdapter(CallsManager.getInstance(),
mCallIdMapper));
mInCallServices.put(componentName, inCallService);
} catch (RemoteException e) {
Log.e(this, e,"Failed to set the in-call adapter.");
return;
}
所以可以看出,Phone的实例就是在InCallService服务启动过程中创建的。
2.InCallController.java(packages\services\telecomm\src\com\android\server\telecom):
上面提到的binder的客户端InCallController位于(packages\services\telecomm\src \com\android\server\telecom)目录,根据manifest文件,它运行在TelecomApp这个应用里,这是一个的5.0 新进程。又根据android:persistent="true"这个属性我们知道,TelecomApp是开机自启动的。
InCallController是在CallsManager的构造函数里创建的,CallsManager又是在TelecomApp的onCreate方法里面创建的,
public void onCreate() {
super.onCreate();
if (UserHandle.myUserId() == UserHandle.USER_OWNER){
// Note: This style of initializationmimics what will be performed once Telecom is
// moved
// to run in the system service. Theemphasis is on ensuring that initialization of all
// telecom classes happens in one placewithout relying on Singleton initialization.
mMissedCallNotifier = newMissedCallNotifier(this);
mPhoneAccountRegistrar = newPhoneAccountRegistrar(this);
mCallsManager =new CallsManager(this, mMissedCallNotifier, mPhoneAccountRegistrar);
CallsManager.initialize(mCallsManager);
mTelecomService = newTelecomServiceImpl(mMissedCallNotifier, mPhoneAccountRegistrar,
mCallsManager, this);
ServiceManager.addService(Context.TELECOM_SERVICE,mTelecomService);
// Start the BluetoothPhoneService
BluetoothPhoneService.start(this);}}
所以,TelecomApp进程的启动过程中,创建了InCallController和CallsManager两个实例,这两个类实例相互关联。
9.以上就是整个拨号联系人姓名如何传到拨号界面上的全部过程,可能比较复杂,涉及到的比较重要的类归纳如下:
Call.java(frameworks\base\telecomm\java\android\telecom)
ParcelableCall.java(frameworks\base\telecomm\java\android\telecom)
Phone.java(frameworks\base\telecomm\java\android\telecom)
InCallService(frameworks\base\telecomm\java\android\telecom)
InCallController.java(packages\services\telecomm\src\com\android\server\telecom)
CallReceiver.java(packages\services\telecomm\src\com\android\server\telecom)
CallsManager(packages\services\telecomm\src\com\android\server\telecom)
CallCardPresenter.java(packages\apps\incallui\src\com\android\incallui)
ContactListAdapter.java(packages\apps\contactscommon\src\com\android\contacts\common)