犀渠玉剑良家子,白马金羁侠少年。这篇文章主要讲述Android7.0 Phone应用源码分析 phone拨号流程分析相关的知识,希望能为你提供帮助。
文章图片
android7.0 Phone拨号流程分析 --- 本文为原创文章,转载请注明出处,http://www.cnblogs.com/lance2016/p/6002371.html1.1 dialer拨号
文章图片
拨号盘点击拨号DialpadFragment的onClick方法会被调用
public void onClick(View view) { int resId = view.getId(); if (resId == R.id.dialpad_floating_action_button) { view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); handleDialButtonPressed(); } else if (resId == R.id.deleteButton) { keyPressed(KeyEvent.KEYCODE_DEL); } else if (resId == R.id.digits) { if (!isDigitsEmpty()) { mDigits.setCursorVisible(true); } } else if (resId == R.id.dialpad_overflow) { mOverflowPopupMenu.show(); } else { Log.wtf(TAG, "Unexpected onClick() event from: " + view); return; } }
handleDialButtonPressed方法处理具体的拨号事件
private void handleDialButtonPressed() { ............ ............ DialerUtils.startActivityWithErrorToast(getActivity(), intent); hideAndClearDialpad(false); }
跟踪DialerUtils的startActivityWithErrorToast方法,内部判断了一些是否有拨号权限的判断后,最后调用TelecomManagerCompat的placeCall事件
public static void placeCall(@Nullable Activity activity, @Nullable TelecomManager telecomManager, @Nullable Intent intent) { if (activity == null || telecomManager == null || intent == null) { return; } if (CompatUtils.isMarshmallowCompatible()) { telecomManager.placeCall(intent.getData(), intent.getExtras()); return; } activity.startActivityForResult(intent, 0); }
这里根据当前系统版本如果是大于等于6.0,则调用TelecomManager的placeCall,否则直接调用startActivity呼出该intent
看看TelecomManager的placeCall方法
android.telecom. TelecomManager public void placeCall(Uri address, Bundle extras) { ITelecomService service = getTelecomService(); if (service != null) { if (address == null) { Log.w(TAG, "Cannot place call to empty address."); } try { service.placeCall(address, extras == null ? new Bundle() : extras, mContext.getOpPackageName());
} catch (RemoteException e) { Log.e(TAG, "Error calling ITelecomService#placeCall", e); } } }
通过aidl接口调用ITelecomService的placeCall方法
1.2 telecomService处理拨号事件TelecomServiceImpl里的mBinderImpl变量是ITelecomService的具体实现类
com.android.server.telecom. TelecomServiceImpl private final ITelecomService.Stub mBinderImpl = new ITelecomService.Stub() {@Override public void placeCall(Uri handle, Bundle extras, String callingPackage) { ……………………………… ……………………………… final UserHandle userHandle = Binder.getCallingUserHandle(); long token = Binder.clearCallingIdentity(); final Intent intent = new Intent(Intent.ACTION_CALL, handle); if (extras != null) { extras.setDefusable(true); intent.putExtras(extras); mUserCallIntentProcessorFactory.create(mContext, userHandle).processIntent
(intent, callingPackage, hasCallAppOp & & hasCallPermission);
} }
这里创建了一个UserCallIntentProcessor对象,并调用其processIntent事件处理,前面提到android 6.0以下用startActivity启动拨号,三方应用拨号都是用这种方式,启动的也是telecom里的UserCallActivity类,同样也是通过UserCallIntentProcessor处理相关事件
com.android.server.telecom.components.UserCallIntentProcessor public void processIntent(Intent intent, String callingPackageName, boolean canCallNonEmergency) { // Ensure call intents are not processed on devices that are not capable of calling. if (!isVoiceCapable()) { return; }String action = intent.getAction(); if (Intent.ACTION_CALL.equals(action) || Intent.ACTION_CALL_PRIVILEGED.equals(action) || Intent.ACTION_CALL_EMERGENCY.equals(action)) { processOutgoingCallIntent(intent, callingPackageName, canCallNonEmergency); } }
进入processOutgoingCallIntent方法
private void processOutgoingCallIntent(Intent intent, String callingPackageName, boolean canCallNonEmergency) { ……………………………… ………………………………sendBroadcastToReceiver(intent); }
内部校验一些是否能拨号权限以及其它操作限制看是否需要直接弹框拒绝,如果都通过了最后会调用sendBroadcastToReceiver方法发送广播
private boolean sendBroadcastToReceiver(Intent intent) { intent.putExtra(CallIntentProcessor.KEY_IS_INCOMING_CALL, false); intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); intent.setClass(mContext, PrimaryCallReceiver.class); Log.d(this, "Sending broadcast as user to CallReceiver"); mContext.sendBroadcastAsUser(intent, UserHandle.SYSTEM); return true; }
该广播直接指定发给PrimaryCallReceiver处理
com.android.server.telecom.components. PrimaryCallReceiver public void onReceive(Context context, Intent intent) { Log.startSession("PCR.oR"); synchronized (getTelecomSystem().getLock()) { getTelecomSystem().getCallIntentProcessor().processIntent(intent); } Log.endSession(); }
接着调用CallIntentProcessor. processIntent(intent)
com.android.server.telecom. CallIntentProcessor public void processIntent(Intent intent) { final boolean isUnknownCall = intent.getBooleanExtra(KEY_IS_UNKNOWN_CALL, false); Log.i(this, "onReceive - isUnknownCall: %s", isUnknownCall); Trace.beginSection("processNewCallCallIntent"); if (isUnknownCall) { processUnknownCallIntent(mCallsManager, intent); } else { processOutgoingCallIntent(mContext, mCallsManager, intent); } Trace.endSection(); }
如果是未知号码如空号由processUnknownCallIntent方法处理
否则调用processOutgoingCallIntent方法
static void processOutgoingCallIntent(Context context, CallsManager callsManager, Intent intent) { ……………………………… ………………………………// Send to CallsManager to ensure the InCallUI gets kicked off before the broadcast returns Call call = callsManager.startOutgoingCall(handle, phoneAccountHandle, clientExtras, initiatingUser); if (call != null) { NewOutgoingCallIntentBroadcaster broadcaster = new NewOutgoingCallIntentBroadcaster( context, callsManager, call, intent, new PhoneNumberUtilsAdapterImpl(), isPrivilegedDialer);
final int result = broadcaster.processIntent();
final boolean success = result == DisconnectCause.NOT_DISCONNECTED; if (!success & & call != null) { disconnectCallAndShowErrorDialog(context, call, result); } } }
方法内部获取一些拨号参数,比如是否视频通话,调用者是否是默认拨号盘应用等,然后调用callsManager的startOutgoingCall方法得到一个call对象,这是一个很重要的方法,来看看它的实现:
Call startOutgoingCall(Uri handle, PhoneAccountHandle phoneAccountHandle, Bundle extras, UserHandle initiatingUser) { boolean isReusedCall = true; Call call = reuseOutgoingCall(handle); // 创建一个call对象 if (call == null) { call = new Call(getNextCallId(), mContext, this, mLock, mConnectionServiceRepository, mContactsAsyncHelper, mCallerInfoAsyncQueryFactory, handle, null /* gatewayInfo */, null /* connectionManagerPhoneAccount */, null /* phoneAccountHandle */, Call.CALL_DIRECTION_OUTGOING /* callDirection */, false /* forceAttachToExistingConnection */, false /* isConference */ ); call.setInitiatingUser(initiatingUser); call.initAnalytics(); isReusedCall = false; }...... ...... ...... ...... ...... ...... List< PhoneAccountHandle> accounts = constructPossiblePhoneAccounts(handle, initiatingUser); // 获取当前激活的卡列表 Log.v(this, "startOutgoingCall found accounts = " + accounts); if (phoneAccountHandle != null) { if (!accounts.contains(phoneAccountHandle)) { phoneAccountHandle = null; } }// 获取当前应该使用哪张卡呼出 if (phoneAccountHandle == null & & accounts.size() > 0 & & !call.isEmergencyCall()) { // No preset account, check if default exists that supports the URI scheme for the // handle and verify it can be used. if(accounts.size() > 1) { // 双激活卡下取通话主卡账户,没有通话主卡则为空 PhoneAccountHandle defaultPhoneAccountHandle = mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(handle.getScheme(), initiatingUser); if (defaultPhoneAccountHandle != null & & accounts.contains(defaultPhoneAccountHandle)) { phoneAccountHandle = defaultPhoneAccountHandle; } } else { // Use the only PhoneAccount that is available // 单激活卡直接取该卡账户) phoneAccountHandle = accounts.get(0); }}call.setTargetPhoneAccount(phoneAccountHandle); // 设置当前通话账户 boolean isPotentialInCallMMICode = isPotentialInCallMMICode(handle); // 检查当前是否允许呼出该电话,比如当前已有一通电话在正在呼出, // 这时候不允许再呼出一路通话(紧急号码除外) if (!isPotentialInCallMMICode & & (!isReusedCall & & !makeRoomForOutgoingCall(call, call.isEmergencyCall()))) { // just cancel at this point. Log.i(this, "No remaining room for outgoing call: %s", call); if (mCalls.contains(call)) { // This call can already exist if it is a reused call, // See {@link #reuseOutgoingCall}. call.disconnect(); } return null; }// 是否需要弹出双卡选择框(双卡下没有指定账户呼出非紧急号码且当前无通话主卡) boolean needsAccountSelection = phoneAccountHandle == null & & accounts.size() > 1 & & !call.isEmergencyCall(); if (needsAccountSelection) { // 设置当前call状态为等待账户选择 // This is the state where the user is expected to select an account call.setState(CallState.SELECT_PHONE_ACCOUNT, "needs account selection"); // Create our own instance to modify (since extras may be Bundle.EMPTY) extras = new Bundle(extras); extras.putParcelableList(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS, accounts); } else { // 设置当前call状态为正在连接 call.setState(CallState.CONNECTING, phoneAccountHandle == null ? "no-handle" : phoneAccountHandle.toString()); }setIntentExtrasAndStartTime(call, extras); // Do not add the call if it is a potential MMI code. if((isPotentialMMICode(handle)||isPotentialInCallMMICode)& & !needsAccountSelection){ call.addListener(this); } else if (!mCalls.contains(call)) { // We check if mCalls already contains the call because we could potentially be reusing // a call which was previously added (See {@link #reuseOutgoingCall}).
addCall(call);
// 添加当前call到call列表 } return call; }
看看addCall的具体实现:
private void addCall(Call call) { Trace.beginSection("addCall"); Log.v(this, "addCall(%s)", call); call.addListener(this); mCalls.add(call); // Specifies the time telecom finished routing the call. This is used by the dialer for // analytics. Bundle extras = call.getIntentExtras(); extras.putLong(TelecomManager.EXTRA_CALL_TELECOM_ROUTING_END_TIME_MILLIS, SystemClock.elapsedRealtime()); updateCallsManagerState(); // onCallAdded for calls which immediately take the foreground (like the first call). for (CallsManagerListener listener : mListeners) { if (Log.SYSTRACE_DEBUG) { Trace.beginSection(listener.getClass().toString() + " addCall"); } listener.onCallAdded(call); if (Log.SYSTRACE_DEBUG) { Trace.endSection(); } } Trace.endSection(); }
这里会遍历call状态变化的观察者并逐个回调通知,这里的观察者比较多,在callsManager创建的时候注册监听的
mListeners.add(mInCallWakeLockController); mListeners.add(statusBarNotifier); mListeners.add(mCallLogManager); mListeners.add(mPhoneStateBroadcaster); mListeners.add(mInCallController); mListeners.add(mCallAudioManager); mListeners.add(missedCallNotifier); mListeners.add(mHeadsetMediaButton); mListeners.add(mProximitySensorManager);
这里说一下mInCallController这个对象,是一个InCallController实例,内部封装了与incallui服务的相关操作,实际上就是一个远程服务代理类,当callsmanager添加一路call时, 回调InCallController的onCallAdded方法,如下:
public void onCallAdded(Call call) { if (!isBoundToServices()) { bindToServices(call); } else { adjustServiceBindingsForEmergency(); Log.i(this, "onCallAdded: %s", System.identityHashCode(call)); // Track the call if we don\'t already know about it. addCall(call); for (Map.Entry< ComponentName, IInCallService> entry : mInCallServices.entrySet()) { ComponentName componentName = entry.getKey(); IInCallService inCallService = entry.getValue(); ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call, true /* includeVideoProvider */, mCallsManager.getPhoneAccountRegistrar()); try { inCallService.addCall(parcelableCall); } catch (RemoteException ignored) { } } } }
最后调用inCallService的addCall方法告诉incallui当前添加了一路通话,incallui收到后会拉起界面,具体过程在此就不详述了,OK回到前面startOutgoingCall的结尾
在成功返回一个call对象之后,新建一个NewOutgoingCallIntentBroadcaster对象,用processIntent方法处理请求
com.android.server.telecom. NewOutgoingCallIntentBroadcaster public int processIntent() { ……………………………… ……………………………… final boolean isPotentialEmergencyNumber = isPotentialEmergencyNumber(number); rewriteCallIntentAction(intent, isPotentialEmergencyNumber); action = intent.getAction(); boolean callImmediately = false; if (Intent.ACTION_CALL.equals(action)) { if (isPotentialEmergencyNumber) { if (!mIsDefaultOrSystemPhoneApp) { launchSystemDialer(intent.getData()); return DisconnectCause.OUTGOING_CANCELED; } else { callImmediately = true; } } } else if (Intent.ACTION_CALL_EMERGENCY.equals(action)) { if (!isPotentialEmergencyNumber) { return DisconnectCause.OUTGOING_CANCELED; } callImmediately = true; } else { return DisconnectCause.INVALID_NUMBER; }if (callImmediately) { String scheme = isUriNumber ? PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL; boolean speakerphoneOn = mIntent.getBooleanExtra( TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, false); int videoState = mIntent.getIntExtra( TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, VideoProfile.STATE_AUDIO_ONLY); mCallsManager.placeOutgoingCall(mCall, Uri.fromParts(scheme, number, null), null, speakerphoneOn, videoState);
}UserHandle targetUser = mCall.getInitiatingUser(); broadcastIntent(intent, number, !callImmediately, targetUser);
return DisconnectCause.NOT_DISCONNECTED; }
该方法主要处理三种类型的call:
普通call Intent.ACTION_CALL
系统call Intent.ACTION_CALL_PRIVILEGED
紧急呼叫call Intent.ACTION_CALL_EMERGENCY
普通call任何应用都可以发起,第三方应用拨号都是使用该intent
系统call只有系统应用才能使用
紧急呼叫call 同样只有系统应用才能使用,并且可以在无卡状态下呼出
对于一个Intent.ACTION_CALL_PRIVILEGED的拨号请求,会根据当前号码是否为紧急号码来转化该intent
private void rewriteCallIntentAction(Intent intent, boolean isPotentialEmergencyNumber) { String action = intent.getAction(); if (Intent.ACTION_CALL_PRIVILEGED.equals(action)) { if (isPotentialEmergencyNumber) {action = Intent.ACTION_CALL_EMERGENCY; } else { action = Intent.ACTION_CALL; } intent.setAction(action); }
}
【Android7.0 Phone应用源码分析 phone拨号流程分析】如果是紧急号码则转化为Intent.ACTION_CALL_EMERGENCY
如果不是紧急号码则转化为Intent.ACTION_CALL
所以实际上处理call只有两种情况Intent.ACTION_CALL和Intent.ACTION_CALL_EMERGENCY
1.对于Intent.ACTION_CALL的处理:
如果当前是紧急号码,会校验调用者是否为系统默认拨号盘
如果是则置变量callImmediately为true,后续直接呼出该电话
如果不是则拉起系统默认拨号盘,当前方法调用返回DisconnectCause.OUTGOING_CANCELED
2. 对于Intent.ACTION_CALL_EMERGENCY的处理:
直接设置变量callImmediately为true,直接呼出该电话
综上所述紧急拨号会直接调用CallsManager的placeOutgoingCall方法后再进入broadcastIntent方法,看看该方法实现
private void broadcastIntent(Intent originalCallIntent,String number, boolean receiverRequired, UserHandle targetUser) { Intent broadcastIntent = new Intent(Intent.ACTION_NEW_OUTGOING_CALL); if (number != null) { broadcastIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, number); } broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); checkAndCopyProviderExtras(originalCallIntent, broadcastIntent); mContext.sendOrderedBroadcastAsUser( broadcastIntent, targetUser, android.Manifest.permission.PROCESS_OUTGOING_CALLS, AppOpsManager.OP_PROCESS_OUTGOING_CALLS, receiverRequired ? new NewOutgoingCallBroadcastIntentReceiver() : null, null,// scheduler Activity.RESULT_OK,// initialCode number,// initialData: initial value for the result data (number to be modified) null); // initialExtras }
发送一个Intent.ACTION_NEW_OUTGOING_CALL广播,对于非紧急拨号,才会生成一个
NewOutgoingCallBroadcastIntentReceive 实例来接收该广播
NewOutgoingCallBroadcastIntentReceiver内部做了一些处理后最后还是调用到CallsManager
的placeOutgoingCall方法,所以该方法是去电的关键方法
com.android.server.telecom. CallsManager public void placeOutgoingCall(Call call, Uri handle, GatewayInfo gatewayInfo, boolean speakerphoneOn, int videoState) { ……………………………… ……………………………… if (call.isEmergencyCall()) { // Emergency -- CreateConnectionProcessor will choose accounts automatically // 如果是紧急号码,则取消已指定的通话卡账户 call.setTargetPhoneAccount(null); new AsyncEmergencyContactNotifier(mContext).execute(); }Final Boolean requireCallCapableAccountByHandle = mContext.getResources().getBoolean( com.android.internal.R.bool.config_requireCallCapableAccountForHandle); if (call.getTargetPhoneAccount() != null || call.isEmergencyCall()) { // If the account has been set, proceed to place the outgoing call. // Otherwise the connection will be initiated when the account is set by the user. // 如果是紧急号码或者已指定通话账户,则创建连接 call.startCreateConnection(mPhoneAccountRegistrar); } else if (mPhoneAccountRegistrar.getCallCapablePhoneAccounts( requireCallCapableAccountByHandle ? call.getHandle().getScheme() : null, false, call.getInitiatingUser()).isEmpty()) { // 如果当前没有激活的卡,则断开此连接 // If there are no call capable accounts, disconnect the call. markCallAsDisconnected(call, new DisconnectCause(DisconnectCause.CANCELED, "No registered PhoneAccounts")); markCallAsRemoved(call); } }
该方法内部做了一些设置操作后,确认可以呼出则call的startCreateConnection方法
com.android.server.telecom.call void startCreateConnection(PhoneAccountRegistrar phoneAccountRegistrar) { if (mCreateConnectionProcessor != null) { return; } mCreateConnectionProcessor = new CreateConnectionProcessor(this, mRepository, this, phoneAccountRegistrar, mContext); mCreateConnectionProcessor.process(); }
新建了一个CreateConnectionProcessor对象,处理连接请求
com.android.server.telecom. CreateConnectionProcessor public void process() { Log.v(this, "process"); clearTimeout(); mAttemptRecords = new ArrayList< > (); if (mCall.getTargetPhoneAccount() != null) { mAttemptRecords.add(new CallAttemptRecord( mCall.getTargetPhoneAccount(), mCall.getTargetPhoneAccount())); } adjustAttemptsForConnectionManager(); adjustAttemptsForEmergency(); mAttemptRecordIterator = mAttemptRecords.iterator(); attemptNextPhoneAccount(); }
进入attemptNextPhoneAccount方法
private void attemptNextPhoneAccount() { ……………………………… ………………………………if (mCallResponse != null & & attempt != null) { PhoneAccountHandle phoneAccount = attempt.connectionManagerPhoneAccount; // 获取ConnectionServiceWrapper对象 mService = mRepository.getService(phoneAccount.getComponentName(), phoneAccount.getUserHandle()); if (mService == null) { attemptNextPhoneAccount(); } else { mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount); mCall.setTargetPhoneAccount(attempt.targetPhoneAccount); mCall.setConnectionService(mService); setTimeoutIfNeeded(mService, attempt); // 已成功获取ConnectionServiceWrapper对象,创建连接
mService.createConnection(mCall, this);
} } else { DisconnectCause disconnectCause = mLastErrorDisconnectCause != null ? mLastErrorDisconnectCause : new DisconnectCause(DisconnectCause.ERROR); notifyCallConnectionFailure(disconnectCause); } }
这里的mService是ConnectionServiceWrapper实例,实际上就是一个包装了绑定远程服务的代理类,看看它的构造方法
看看ConnectionServiceWrapper的构造函数
ConnectionServiceWrapper( ComponentName componentName, ConnectionServiceRepository connectionServiceRepository, PhoneAccountRegistrar phoneAccountRegistrar, CallsManager callsManager, Context context, TelecomSystem.SyncRoot lock, UserHandle userHandle) { super(ConnectionService.SERVICE_INTERFACE, componentName, context, lock, userHandle); mConnectionServiceRepository = connectionServiceRepository; phoneAccountRegistrar.addListener(new PhoneAccountRegistrar.Listener() { // TODO -- Upon changes to PhoneAccountRegistrar, need to re-wire connections // To do this, we must proxy remote ConnectionService objects }); mPhoneAccountRegistrar = phoneAccountRegistrar; mCallsManager = callsManager; }
这里的ConnectionService.SERVICE_INTERFACE就是"android.telecom.ConnectionService"
也就是它所绑定的远程服务的action
获取该对象后调用createConnection方法
com.android.server.telecom. ConnectionServiceWrapper public void createConnection(final Call call, final CreateConnectionResponse response) { Log.d(this, "createConnection(%s) via %s.", call, getComponentName()); BindCallback callback = new BindCallback() { @Override public void onSuccess() { ...... ...... ...... ...... ...... ...... ...... ...... try { mServiceInterface.createConnection( call.getConnectionManagerPhoneAccount(), callId, new ConnectionRequest( call.getTargetPhoneAccount(), call.getHandle(), extras, call.getVideoState(), callId), call.shouldAttachToExistingConnection(), call.isUnknown()); } ...... ...... ...... ......@Override public void onFailure() { Log.e(this, new Exception(), "Failure to call %s", getComponentName()); response.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.ERROR)); } }; mBinder.bind(callback, call); }
这里的mBinder对象是ConnectionServiceWrapper的父类ServiceBinder里的一个内部类
封装了绑定远程服务的一些操作,若当前还未绑定服务,则直接调用bindService获取远程服务的aidl接口,成功获取到aidl接口后将其赋值给mServiceInterface,如下:
@Override protected void setServiceInterface(IBinder binder) { if (binder == null) { // We have lost our service connection. Notify the world that this service is done. // We must notify the adapter before CallsManager. The adapter will force any pending // outgoing calls to try the next service. This needs to happen before CallsManager // tries to clean up any calls still associated with this service. handleConnectionServiceDeath(); mCallsManager.handleConnectionServiceDeath(this); mServiceInterface = null; } else { mServiceInterface = IConnectionService.Stub.asInterface(binder); addConnectionServiceAdapter(mAdapter); } }
最终不管是初次绑定还是之前已绑定服务,调用 mBinder.bind(callback, call)成功后都会回到Callback的onSuccess方法,接着调用远程服务的createConnection方法
mServiceInterface.createConnection( call.getConnectionManagerPhoneAccount(), callId, new ConnectionRequest( call.getTargetPhoneAccount(), call.getHandle(), extras, call.getVideoState(), callId), call.shouldAttachToExistingConnection(), call.isUnknown());
接下来的流程就到了远程服务的createConnection实现了
1.3 telecomFramework处理连接请求查找IConnectionService的实现类,是ConnectionService的匿名内部类
android.telecom. ConnectionService private final IBinder mBinder = new IConnectionService.Stub() {@Override public void createConnection( PhoneAccountHandle connectionManagerPhoneAccount, String id, ConnectionRequest request, boolean isIncoming, boolean isUnknown) { SomeArgs args = SomeArgs.obtain(); args.arg1推荐阅读
- Android缓存
- [gitbook] Android框架分析系列之Android Binder详解
- cdmc2016数据挖掘竞赛题目Android Malware Classification
- Android开发学习——基础学习
- 四 Android Capabilities讲解
- 二 APPIUM Android自动化 测试初体验
- 六 APPIUM Android 定位方式
- android加速度传感器---摇一摇
- Android studio第四次作业