当前位置:   article > 正文

Android电话拨打流程源码分析

android handledialbuttonclickwithemptydigits

前面分析了电话拨号界面及电话呼叫界面,由于Android的电话Phone设计的很复杂,因此先从UI层入手分析。想要了解Android的电话拨号UI,请查看Android电话拨号UI分析,电话拨号UI在Contacts包中。想要了解Android电话呼叫UI,请查看Android电话Phone UI分析,该UI在Phone包中,了解完电话想要UI后,还必须首先了解Android的Phone设计框架,Android电话Phone设计框架介绍介绍了Phone的框架设计及Phone进程的启动,本文以源码的形式介绍Android的电话拨打流程。点击Launcher上的拨号图标,首先进入电话拨号界面,前面已经分析了,该UI在Contacts包中,启动显示的是DialtactsActivity,关于DialtactsActivity的布局解析、UI布局在Android电话拨号UI分析中有详细的分析,这里不在重复介绍。我们从点击拨号按钮开始分析电话的拨号流程:

DialpadFragment.java

  1. public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
  2. ...
  3. // Check whether we should show the onscreen "Dial" button.
  4. mDialButton = mAdditionalButtonsRow.findViewById(R.id.dialButton);
  5. if (r.getBoolean(R.bool.config_show_onscreen_dial_button)) {
  6. mDialButton.setOnClickListener(this);
  7. } else {
  8. mDialButton.setVisibility(View.GONE); // It's VISIBLE by default
  9. mDialButton = null;
  10. }
  11. ...
  12. }

拨号按钮的单击事件响应:

  1. public void onClick(View view) {
  2. switch (view.getId()) {
  3. case R.id.dialButton: {
  4. mHaptic.vibrate();
  5. dialButtonPressed();
  6. return;
  7. }
  8. ...
  9. }
  10. }

调用dialButtonPressed()函数发起电话呼叫

  1. public void dialButtonPressed() {
  2. if(mDigits == null){
  3. Log.e(TAG,"dialButtonPressed,mDigits == null");
  4. return;
  5. }
  6. //未输入号码处理
  7. if (isDigitsEmpty()) {
  8. handleDialButtonClickWithEmptyDigits();
  9. } else {
  10. final String number = mDigits.getText().toString();
  11. // "persist.radio.otaspdial" is a temporary hack needed for one carrier's automated
  12. // test equipment.
  13. if (number != null&& !TextUtils.isEmpty(mProhibitedPhoneNumberRegexp)
  14. && number.matches(mProhibitedPhoneNumberRegexp)
  15. && (SystemProperties.getInt("persist.radio.otaspdial", 0) != 1)) {
  16. Log.i(TAG, "The phone number is prohibited explicitly by a rule.");
  17. if (getActivity() != null) {
  18. DialogFragment dialogFragment = ErrorDialogFragment.newInstance(
  19. R.string.dialog_phone_call_prohibited_title);
  20. dialogFragment.show(getFragmentManager(), "phone_prohibited_dialog");
  21. }
  22. //号码输入不正确.
  23. mDigits.getText().clear();
  24. } else if(number != null && (number.startsWith(",") || number.startsWith(";"))){
  25. mDigits.getText().clear();
  26. if (getActivity() != null) {
  27. Toast.makeText(getActivity(), getText(R.string.invalid_number),
  28. Toast.LENGTH_SHORT).show();
  29. }
  30. } else {
  31. //启动电话呼叫界面
  32. final Intent intent = ContactsUtils.getCallIntent(number,(getActivity() instanceof DialtactsActivity ?
  33. ((DialtactsActivity)getActivity()).getCallOrigin() : null));
  34. startActivity(intent);
  35. mClearDigitsOnStop = true;
  36. mDigits.getText().clear();
  37. if(mFlagIntentNumber){
  38. getActivity().finish();
  39. }
  40. }
  41. }
  42. }

函数首先对输入的号码进行检查,如果没有输入号码,直接按下拨号按钮,则调用handleDialButtonClickWithEmptyDigits函数来处理

  1. private void handleDialButtonClickWithEmptyDigits() {
  2. if (phoneIsCdma() && phoneIsOffhook()) {
  3. // This is really CDMA specific. On GSM is it possible
  4. // to be off hook and wanted to add a 3rd party using
  5. // the redial feature.
  6. startActivity(newFlashIntent());
  7. } else {
  8. if (mDigits != null && !TextUtils.isEmpty(mLastNumberDialed)) {
  9. // Recall the last number dialed.
  10. mDigits.setText(mLastNumberDialed);
  11. // ...and move the cursor to the end of the digits string,
  12. // so you'll be able to delete digits using the Delete
  13. // button (just as if you had typed the number manually.)
  14. //
  15. // Note we use mDigits.getText().length() here, not
  16. // mLastNumberDialed.length(), since the EditText widget now
  17. // contains a *formatted* version of mLastNumberDialed (due to
  18. // mTextWatcher) and its length may have changed.
  19. mDigits.setSelection(mDigits.getText().length());
  20. } else {
  21. // There's no "last number dialed" or the
  22. // background query is still running. There's
  23. // nothing useful for the Dial button to do in
  24. // this case. Note: with a soft dial button, this
  25. // can never happens since the dial button is
  26. // disabled under these conditons.
  27. playTone(ToneGenerator.TONE_PROP_NACK);
  28. }
  29. }
  30. }

如果号码输入正确合法,则使用ContactsUtils工具类来创建一个Intent。

DialtactsActivity.java

  1. public String getCallOrigin() {
  2. return !isDialIntent(getIntent()) ? CALL_ORIGIN_DIALTACTS : null;
  3. }
  4. /** Returns true if the given intent contains a phone number to populate the dialer with */
  5. private boolean isDialIntent(Intent intent) {
  6. final String action = intent.getAction();
  7. if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) {
  8. return true;
  9. }
  10. if (Intent.ACTION_VIEW.equals(action)) {
  11. final Uri data = intent.getData();
  12. if (data != null && Constants.SCHEME_TEL.equals(data.getScheme())) {
  13. return true;
  14. }
  15. }
  16. return false;
  17. }

从Launcher点击拨号图标进入的,因此isDialIntent返回true,getCallOrigin返回null

ContactsUtils.java

  1. public static Intent getCallIntent(String number, String callOrigin) {
  2. return getCallIntent(getCallUri(number), callOrigin);
  3. }
  4. public static Intent getCallIntent(Uri uri, String callOrigin) {
  5. final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, uri);
  6. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  7. if (callOrigin != null) {
  8. intent.putExtra(DialtactsActivity.EXTRA_CALL_ORIGIN, callOrigin);
  9. }
  10. return intent;
  11. }

action为Intent.ACTION_CALL_PRIVILEGED,因此使用隐式启动OutgoingCallBroadcaster

因此Phone进程中的OutgoingCallBroadcaster将被启动。google对电话拨号步骤有详细的说明:

/*
* Here's the most typical outgoing call sequence:
*
* (1) OutgoingCallBroadcaster receives a CALL intent and sends the
* NEW_OUTGOING_CALL broadcast
*
* (2) The broadcast finally reaches OutgoingCallReceiver, which stashes
* away a copy of the original CALL intent and launches
* SipCallOptionHandler
*
* (3) SipCallOptionHandler decides whether this is a PSTN or SIP call (and
* in some cases brings up a dialog to let the user choose), and
* ultimately calls CallController.placeCall() (from the
* setResultAndFinish() method) with the stashed-away intent from step
* (2) as the "intent" parameter.
*
* (4) Here in CallController.placeCall() we read the phone number or SIP
* address out of the intent and actually initiate the call, and
* simultaneously launch the InCallScreen to display the in-call UI.
*
* (5) We handle various errors by directing the InCallScreen to
* display error messages or dialogs (via the InCallUiState
* "pending call status code" flag), and in some cases we also
* sometimes continue working in the background to resolve the
* problem (like in the case of an emergency call while in
* airplane mode). Any time that some onscreen indication to the
* user needs to change, we update the "status dialog" info in
* the inCallUiState and (re)launch the InCallScreen to make sure
* it's visible.
*/


如OutgoingCallBroadcaster接收 CALL 和CALL_PRIVILEGED 两种Intents,然后广播出ACTION_NEW_OUTGOING_CALL intent,让别的应用程序有机会去监视这些intent,最后这些呼叫intent又被自己收到转换,启动InCallScreen.

src\com\android\phone\OutgoingCallBroadcaster.java

  1. protected void onCreate(Bundle icicle) {
  2. super.onCreate(icicle);
  3. setContentView(R.layout.outgoing_call_broadcaster);
  4. mWaitingSpinner = (ProgressBar) findViewById(R.id.spinner);
  5. Intent intent = getIntent();
  6. if (DBG) {
  7. final Configuration configuration = getResources().getConfiguration();
  8. Log.v(TAG, "onCreate: this = " + this + ", icicle = " + icicle);
  9. Log.v(TAG, " - getIntent() = " + intent);
  10. Log.v(TAG, " - configuration = " + configuration);
  11. }
  12. if (icicle != null) {
  13. //icicle不为空,表示重新初始化先前关闭的OutgoingCallBroadcaster,
  14. // In practice this happens very rarely (because the lifetime
  15. // of this activity is so short!), but it *can* happen if the
  16. // framework detects a configuration change at exactly the
  17. // right moment;
  18. // In this case, do nothing. Our onCreate() method has already
  19. // run once (with icicle==null the first time), which means
  20. // that the NEW_OUTGOING_CALL broadcast for this new call has
  21. // already been sent.
  22. Log.i(TAG, "onCreate: non-null icicle! "
  23. + "Bailing out, not sending NEW_OUTGOING_CALL broadcast...");
  24. return;
  25. }
  26. //处理得到的intent
  27. processIntent(intent);
  28. if (DBG) Log.v(TAG, "At the end of onCreate(). isFinishing(): " + isFinishing());
  29. }

函数直接调用processIntent函数处理前面发送过来的intent,该方法可以处理以下三种actions,

CALL (action for usual outgoing voicecalls)

CALL_PRIVILEGED (can come from built-inapps like contacts / voice dialer / bluetooth)

CALL_EMERGENCY (from the EmergencyDialerthat's reachable from the lockscreen.)

对于数据为tel: URI的电话处理流程为:OutgoingCallReceiver -> SipCallOptionHandler ->InCallScreen.

对于数据为sip: URI的网络电话,则跳过NEW_OUTGOING_CALL广播,直接调用SipCallOptionHandler处理

对于数据为voicemail: URIs的语音信箱处理同电话处理流程类似

  1. private void processIntent(Intent intent) {
  2. if (DBG) {
  3. Log.v(TAG, "processIntent() = " + intent + ", thread: " + Thread.currentThread());
  4. }
  5. final Configuration configuration = getResources().getConfiguration();
  6. // 电话拨号只对具有语音通信能力的设备而言
  7. if (!PhoneGlobals.sVoiceCapable) {
  8. Log.i(TAG, "This device is detected as non-voice-capable device.");
  9. handleNonVoiceCapable(intent);
  10. return;
  11. }
  12. //得到相应的Action
  13. String action = intent.getAction();
  14. //从Intent中取出电话号码
  15. String number = PhoneNumberUtils.getNumberFromIntent(intent, this);
  16. //电话号码检查
  17. if (number != null) {
  18. if (!PhoneNumberUtils.isUriNumber(number)) {
  19. //根据键盘map将字符转换为相应的数字
  20. number = PhoneNumberUtils.convertKeypadLettersToDigits(number);
  21. number = PhoneNumberUtils.stripSeparators(number);
  22. }
  23. } else {
  24. Log.w(TAG, "The number obtained from Intent is null.");
  25. }
  26. // 如果callNow为true,表示当前为不允许拦截的如紧急拨号,这种情形下就无需这NEW_OUTGOING_CALL流程
  27. boolean callNow;
  28. if (getClass().getName().equals(intent.getComponent().getClassName())) {
  29. // If we were launched directly from the OutgoingCallBroadcaster,
  30. // not one of its more privileged aliases, then make sure that
  31. // only the non-privileged actions are allowed.
  32. if (!Intent.ACTION_CALL.equals(intent.getAction())) {
  33. Log.w(TAG, "Attempt to deliver non-CALL action; forcing to CALL");
  34. intent.setAction(Intent.ACTION_CALL);
  35. }
  36. }
  37. // 检查当前号码是否为紧急号码,只有CALL_PRIVILEGED和CALL_EMERGENCY类型的intent才允许拨打紧急号码
  38. // (Note that the ACTION_CALL check below depends on the result of
  39. // isPotentialLocalEmergencyNumber() rather than just plain
  40. // isLocalEmergencyNumber()
  41. // 100%确保第三方应用不允许通过传递如"9111234" 这种无效号码来拨打紧急号码
  42. final boolean isExactEmergencyNumber =(number != null) && PhoneNumberUtils.isLocalEmergencyNumber(number, this);
  43. final boolean isPotentialEmergencyNumber = (number != null) && PhoneNumberUtils.isPotentialLocalEmergencyNumber(number, this);
  44. if (VDBG) {
  45. Log.v(TAG, " - Checking restrictions for number '" + number + "':");
  46. Log.v(TAG, " isExactEmergencyNumber = " + isExactEmergencyNumber);
  47. Log.v(TAG, " isPotentialEmergencyNumber = " + isPotentialEmergencyNumber);
  48. }
  49. if (Intent.ACTION_CALL_PRIVILEGED.equals(action)) {
  50. if (isPotentialEmergencyNumber) {
  51. Log.i(TAG, "ACTION_CALL_PRIVILEGED is used while the number is a potential"
  52. + " emergency number. Use ACTION_CALL_EMERGENCY as an action instead.");
  53. action = Intent.ACTION_CALL_EMERGENCY;
  54. } else {
  55. action = Intent.ACTION_CALL;
  56. }
  57. if (DBG) Log.v(TAG, " - updating action from CALL_PRIVILEGED to " + action);
  58. intent.setAction(action);
  59. }
  60. //如果普通拨打的号码为紧急号码,则启动电话拨号器
  61. if (Intent.ACTION_CALL.equals(action)) {
  62. if (isPotentialEmergencyNumber) {
  63. Log.w(TAG, "Cannot call potential emergency number '" + number
  64. + "' with CALL Intent " + intent + ".");
  65. Log.i(TAG, "Launching default dialer instead...");
  66. //启动默认的电话拨号器DialtactsActivity
  67. Intent invokeFrameworkDialer = new Intent();
  68. invokeFrameworkDialer.setClassName("com.android.contacts","com.android.contacts.DialtactsActivity");
  69. invokeFrameworkDialer.setAction(Intent.ACTION_DIAL);
  70. invokeFrameworkDialer.setData(intent.getData());
  71. if (DBG) Log.v(TAG, "onCreate(): calling startActivity for Dialer: " + invokeFrameworkDialer);
  72. startActivity(invokeFrameworkDialer);
  73. finish();
  74. return;
  75. }
  76. callNow = false;
  77. //如果是紧急拨号,1.通过紧急拨号器拨号;2.ACTION_CALL_PRIVILEGED拨打紧急号码;将callNow设置为true
  78. } else if (Intent.ACTION_CALL_EMERGENCY.equals(action)) {
  79. if (!isPotentialEmergencyNumber) {
  80. Log.w(TAG, "Cannot call non-potential-emergency number " + number
  81. + " with EMERGENCY_CALL Intent " + intent + "."
  82. + " Finish the Activity immediately.");
  83. finish();
  84. return;
  85. }
  86. callNow = true;
  87. } else {
  88. Log.e(TAG, "Unhandled Intent " + intent + ". Finish the Activity immediately.");
  89. finish();
  90. return;
  91. }
  92. //唤醒屏幕
  93. PhoneGlobals.getInstance().wakeUpScreen();
  94. // If number is null, we're probably trying to call a non-existent voicemail number,
  95. // send an empty flash or something else is fishy. Whatever the problem, there's no
  96. // number, so there's no point in allowing apps to modify the number.
  97. if (TextUtils.isEmpty(number)) {
  98. if (intent.getBooleanExtra(EXTRA_SEND_EMPTY_FLASH, false)) {
  99. Log.i(TAG, "onCreate: SEND_EMPTY_FLASH...");
  100. PhoneUtils.sendEmptyFlash(PhoneGlobals.getPhone());
  101. finish();
  102. return;
  103. } else {
  104. Log.i(TAG, "onCreate: null or empty number, setting callNow=true...");
  105. callNow = true;
  106. }
  107. }
  108. //如果是紧急拨号,直接启动拨号界面
  109. if (callNow) {
  110. Log.i(TAG, "onCreate(): callNow case! Calling placeCall(): " + intent);
  111. // Initiate the outgoing call, and simultaneously launch the
  112. // InCallScreen to display the in-call UI:
  113. PhoneGlobals.getInstance().callController.placeCall(intent);
  114. }
  115. // Remember the call origin so that users will be able to see an appropriate screen
  116. // after the phone call. This should affect both phone calls and SIP calls.
  117. final String callOrigin = intent.getStringExtra(PhoneGlobals.EXTRA_CALL_ORIGIN);
  118. if (callOrigin != null) {
  119. if (DBG) Log.v(TAG, " - Call origin is passed (" + callOrigin + ")");
  120. PhoneGlobals.getInstance().setLatestActiveCallOrigin(callOrigin);
  121. } else {
  122. if (DBG) Log.v(TAG, " - Call origin is not passed. Reset current one.");
  123. PhoneGlobals.getInstance().resetLatestActiveCallOrigin();
  124. }
  125. // For now, SIP calls will be processed directly without a
  126. // NEW_OUTGOING_CALL broadcast.
  127. //
  128. // TODO: In the future, though, 3rd party apps *should* be allowed to
  129. // intercept outgoing calls to SIP addresses as well. To do this, we should
  130. // (1) update the NEW_OUTGOING_CALL intent documentation to explain this
  131. // case, and (2) pass the outgoing SIP address by *not* overloading the
  132. // EXTRA_PHONE_NUMBER extra, but instead using a new separate extra to hold
  133. // the outgoing SIP address. (Be sure to document whether it's a URI or just
  134. // a plain address, whether it could be a tel: URI, etc.)
  135. Uri uri = intent.getData();
  136. String scheme = uri.getScheme();
  137. if (Constants.SCHEME_SIP.equals(scheme) || PhoneNumberUtils.isUriNumber(number)) {
  138. Log.i(TAG, "The requested number was detected as SIP call.");
  139. startSipCallOptionHandler(this, intent, uri, number);
  140. finish();
  141. return;
  142. }
  143. Intent broadcastIntent = new Intent(Intent.ACTION_NEW_OUTGOING_CALL);
  144. if (number != null) {
  145. broadcastIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, number);
  146. }
  147. PhoneUtils.checkAndCopyPhoneProviderExtras(intent, broadcastIntent);
  148. broadcastIntent.putExtra(EXTRA_ALREADY_CALLED, callNow);
  149. broadcastIntent.putExtra(EXTRA_ORIGINAL_URI, uri.toString());
  150. // Need to raise foreground in-call UI as soon as possible while allowing 3rd party app
  151. // to intercept the outgoing call.
  152. broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
  153. if (DBG) Log.v(TAG, " - Broadcasting intent: " + broadcastIntent + ".");
  154. //发送超时消息,当OutgoingCallReceiver在指定的时间内还未接受到广播时,显示超时
  155. mHandler.sendEmptyMessageDelayed(EVENT_OUTGOING_CALL_TIMEOUT,
  156. OUTGOING_CALL_TIMEOUT_THRESHOLD);
  157. //发送ACTION_NEW_OUTGOING_CALL广播
  158. sendOrderedBroadcastAsUser(broadcastIntent, UserHandle.OWNER,
  159. PERMISSION, new OutgoingCallReceiver(),
  160. null, // scheduler
  161. Activity.RESULT_OK, // initialCode
  162. number, // initialData: initial value for the result data
  163. null); // initialExtras
  164. }
首先获取Intent对象,获取拨出的号码。接着判断号码是否为紧急号码,如果是紧急号码,将callNow变量赋值为true,启动InCallScreen,并发送Intent.ACTION_NEW_OUTGOING_CALL广播。
  1. public void onReceive(Context context, Intent intent) {
  2. mHandler.removeMessages(EVENT_OUTGOING_CALL_TIMEOUT);
  3. doReceive(context, intent);
  4. if (DBG) Log.v(TAG, "OutgoingCallReceiver is going to finish the Activity itself.");
  5. finish();
  6. }

直接调用函数doReceive函数来处理ntent.ACTION_NEW_OUTGOING_CALL广播

  1. public void doReceive(Context context, Intent intent) {
  2. if (DBG) Log.v(TAG, "doReceive: " + intent);
  3. boolean alreadyCalled;
  4. String number;
  5. String originalUri;
  6. alreadyCalled = intent.getBooleanExtra(OutgoingCallBroadcaster.EXTRA_ALREADY_CALLED, false);
  7. if (alreadyCalled) {
  8. if (DBG) Log.v(TAG, "CALL already placed -- returning.");
  9. return;
  10. }
  11. number = getResultData();
  12. if (VDBG) Log.v(TAG, "- got number from resultData: '" + number + "'");
  13. final PhoneGlobals app = PhoneGlobals.getInstance();
  14. //如果电话支持Otasp
  15. if (TelephonyCapabilities.supportsOtasp(app.phone)) {
  16. boolean activateState = (app.cdmaOtaScreenState.otaScreenState
  17. == OtaUtils.CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION);
  18. boolean dialogState = (app.cdmaOtaScreenState.otaScreenState
  19. == OtaUtils.CdmaOtaScreenState.OtaScreenState
  20. .OTA_STATUS_SUCCESS_FAILURE_DLG);
  21. boolean isOtaCallActive = false;
  22. if ((app.cdmaOtaScreenState.otaScreenState
  23. == OtaUtils.CdmaOtaScreenState.OtaScreenState.OTA_STATUS_PROGRESS)
  24. || (app.cdmaOtaScreenState.otaScreenState
  25. == OtaUtils.CdmaOtaScreenState.OtaScreenState.OTA_STATUS_LISTENING)) {
  26. isOtaCallActive = true;
  27. }
  28. if (activateState || dialogState) {
  29. // The OTASP sequence is active, but either (1) the call
  30. // hasn't started yet, or (2) the call has ended and we're
  31. // showing the success/failure screen. In either of these
  32. // cases it's OK to make a new outgoing call, but we need
  33. // to take down any OTASP-related UI first.
  34. if (dialogState) app.dismissOtaDialogs();
  35. app.clearOtaState();
  36. app.clearInCallScreenMode();
  37. } else if (isOtaCallActive) {
  38. // The actual OTASP call is active. Don't allow new
  39. // outgoing calls at all from this state.
  40. Log.w(TAG, "OTASP call is active: disallowing a new outgoing call.");
  41. return;
  42. }
  43. }
  44. if (number == null) {
  45. if (DBG) Log.v(TAG, "CALL cancelled (null number), returning...");
  46. return;
  47. } else if (TelephonyCapabilities.supportsOtasp(app.phone)
  48. && (app.phone.getState() != PhoneConstants.State.IDLE)
  49. && (app.phone.isOtaSpNumber(number))) {
  50. if (DBG) Log.v(TAG, "Call is active, a 2nd OTA call cancelled -- returning.");
  51. return;
  52. } else if (PhoneNumberUtils.isPotentialLocalEmergencyNumber(number, context)) {
  53. Log.w(TAG, "Cannot modify outgoing call to emergency number " + number + ".");
  54. return;
  55. }
  56. originalUri = intent.getStringExtra(OutgoingCallBroadcaster.EXTRA_ORIGINAL_URI);
  57. if (originalUri == null) {
  58. Log.e(TAG, "Intent is missing EXTRA_ORIGINAL_URI -- returning.");
  59. return;
  60. }
  61. Uri uri = Uri.parse(originalUri);
  62. number = PhoneNumberUtils.convertKeypadLettersToDigits(number);
  63. number = PhoneNumberUtils.stripSeparators(number);
  64. if (DBG) Log.v(TAG, "doReceive: proceeding with call...");
  65. if (VDBG) Log.v(TAG, "- uri: " + uri);
  66. if (VDBG) Log.v(TAG, "- actual number to dial: '" + number + "'");
  67. startSipCallOptionHandler(context, intent, uri, number);
  68. }

OutgoingCallReceiver是OutgoingCallBroadcaster的一个内部类,作用是接收OutgoingCallBroadcaster发送的广播,判断是否已经启动InCallScreen。没有启动的话就进行一些初始化,如:对OTA进行初始化。接收到广播之后,从Intent里面取出电话号码及其URi。然后设置Intent为ACTION_CALL,并带上号码和uri。启动InCallScreen。关闭OutgoingCallReceiver。

OTA:Over-the-Air Technology 空中下载技术,是通过移动通信(GSM或CDMA)的空中接口对SIM卡数据及应用进行远程管理的技术。空中接口可以采用WAP、GPRS、CDMA1X及短消息技术。OTA技术的应用,使得移动通信不仅可以提供语音和数据服务,而且还能提供新业务下载。

  1. private void startSipCallOptionHandler(Context context, Intent intent,
  2. Uri uri, String number) {
  3. if (VDBG) {
  4. Log.i(TAG, "startSipCallOptionHandler...");
  5. Log.i(TAG, "- intent: " + intent);
  6. Log.i(TAG, "- uri: " + uri);
  7. Log.i(TAG, "- number: " + number);
  8. }
  9. //创建原始电话拨号intent的副本
  10. Intent newIntent = new Intent(Intent.ACTION_CALL, uri);
  11. newIntent.putExtra(EXTRA_ACTUAL_NUMBER_TO_DIAL, number);
  12. PhoneUtils.checkAndCopyPhoneProviderExtras(intent, newIntent);
  13. Intent selectPhoneIntent = new Intent(ACTION_SIP_SELECT_PHONE, uri);
  14. selectPhoneIntent.setClass(context, SipCallOptionHandler.class);
  15. selectPhoneIntent.putExtra(EXTRA_NEW_CALL_INTENT, newIntent);
  16. selectPhoneIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  17. if (DBG) {
  18. Log.v(TAG, "startSipCallOptionHandler(): " +
  19. "calling startActivity: " + selectPhoneIntent);
  20. }
  21. //启动电话类型选择界面
  22. context.startActivity(selectPhoneIntent);
  23. }
电话类型选择处理:1.读取用户设置;2.弹出对话框让用户选择
src\com\android\phone\SipCallOptionHandler.java

  1. public void onCreate(Bundle savedInstanceState) {
  2. super.onCreate(savedInstanceState);
  3. Intent intent = getIntent();
  4. String action = intent.getAction();
  5. if (!OutgoingCallBroadcaster.ACTION_SIP_SELECT_PHONE.equals(action)) {
  6. Log.wtf(TAG, "onCreate: got intent action '" + action + "', expected "
  7. + OutgoingCallBroadcaster.ACTION_SIP_SELECT_PHONE);
  8. finish();
  9. return;
  10. }
  11. //取出原始电话拨号intent的副本
  12. mIntent = (Intent) intent.getParcelableExtra(OutgoingCallBroadcaster.EXTRA_NEW_CALL_INTENT);
  13. if (mIntent == null) {
  14. finish();
  15. return;
  16. }
  17. // Allow this activity to be visible in front of the keyguard.
  18. getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
  19. // - If it's a sip: URI, this is definitely a SIP call, regardless
  20. // of whether the data is a SIP address or a regular phone
  21. // number.
  22. // - If this is a tel: URI but the data contains an "@" character
  23. // (see PhoneNumberUtils.isUriNumber()) we consider that to be a
  24. // SIP number too.
  25. boolean voipSupported = PhoneUtils.isVoipSupported();
  26. if (DBG) Log.v(TAG, "voipSupported: " + voipSupported);
  27. mSipProfileDb = new SipProfileDb(this);
  28. mSipSharedPreferences = new SipSharedPreferences(this);
  29. mCallOption = mSipSharedPreferences.getSipCallOption();
  30. if (DBG) Log.v(TAG, "Call option: " + mCallOption);
  31. Uri uri = mIntent.getData();
  32. String scheme = uri.getScheme();
  33. mNumber = PhoneNumberUtils.getNumberFromIntent(mIntent, this);
  34. boolean isInCellNetwork = PhoneGlobals.getInstance().phoneMgr.isRadioOn();
  35. boolean isKnownCallScheme = Constants.SCHEME_TEL.equals(scheme)
  36. || Constants.SCHEME_SIP.equals(scheme);
  37. boolean isRegularCall = Constants.SCHEME_TEL.equals(scheme)
  38. && !PhoneNumberUtils.isUriNumber(mNumber);
  39. // Bypass the handler if the call scheme is not sip or tel.
  40. if (!isKnownCallScheme) {
  41. setResultAndFinish();
  42. return;
  43. }
  44. // Check if VoIP feature is supported.
  45. if (!voipSupported) {
  46. if (!isRegularCall) {
  47. showDialog(DIALOG_NO_VOIP);
  48. } else {
  49. setResultAndFinish();
  50. }
  51. return;
  52. }
  53. if (!PhoneUtils.hasPhoneProviderExtras(mIntent)) {
  54. if (!isNetworkConnected()) {
  55. if (!isRegularCall) {
  56. //显示无网络错误提示对话框
  57. showDialog(DIALOG_NO_INTERNET_ERROR);
  58. return;
  59. }
  60. } else {
  61. if (mCallOption.equals(Settings.System.SIP_ASK_ME_EACH_TIME)
  62. && isRegularCall && isInCellNetwork) {
  63. //显示电话类型选择对话框
  64. showDialog(DIALOG_SELECT_PHONE_TYPE);
  65. return;
  66. }
  67. if (!mCallOption.equals(Settings.System.SIP_ADDRESS_ONLY)
  68. || !isRegularCall) {
  69. mUseSipPhone = true;
  70. }
  71. }
  72. }
  73. if (mUseSipPhone) {
  74. // If there is no sip profile and it is a regular call, then we
  75. // should use pstn network instead.
  76. if ((mSipProfileDb.getProfilesCount() > 0) || !isRegularCall) {
  77. startGetPrimarySipPhoneThread();
  78. return;
  79. } else {
  80. mUseSipPhone = false;
  81. }
  82. }
  83. setResultAndFinish();
  84. }
选择SIP拨号还是PSTN拨号

  1. private void setResultAndFinish() {
  2. runOnUiThread(new Runnable() {
  3. public void run() {
  4. if (mOutgoingSipProfile != null) {
  5. if (!isNetworkConnected()) {
  6. showDialog(DIALOG_NO_INTERNET_ERROR);
  7. return;
  8. }
  9. if (DBG) Log.v(TAG, "primary SIP URI is " +
  10. mOutgoingSipProfile.getUriString());
  11. createSipPhoneIfNeeded(mOutgoingSipProfile);
  12. mIntent.putExtra(OutgoingCallBroadcaster.EXTRA_SIP_PHONE_URI,
  13. mOutgoingSipProfile.getUriString());
  14. if (mMakePrimary) {
  15. mSipSharedPreferences.setPrimaryAccount(
  16. mOutgoingSipProfile.getUriString());
  17. }
  18. }
  19. //mUseSipPhone在SipCallOptionHandler的onCreate函数中被设置为false
  20. if (mUseSipPhone && mOutgoingSipProfile == null) {
  21. showDialog(DIALOG_START_SIP_SETTINGS);
  22. return;
  23. } else {
  24. // Woo hoo -- it's finally OK to initiate the outgoing call!
  25. PhoneGlobals.getInstance().callController.placeCall(mIntent);
  26. }
  27. finish();
  28. }
  29. });
  30. }
src\com\android\phone\CallController.java
  1. public void placeCall(Intent intent) {
  2. log("placeCall()... intent = " + intent);
  3. if (VDBG) log("extras = " + intent.getExtras());
  4. final InCallUiState inCallUiState = mApp.inCallUiState;
  5. if (intent == null) {
  6. Log.wtf(TAG, "placeCall: called with null intent");
  7. throw new IllegalArgumentException("placeCall: called with null intent");
  8. }
  9. String action = intent.getAction();
  10. Uri uri = intent.getData();
  11. if (uri == null) {
  12. Log.wtf(TAG, "placeCall: intent had no data");
  13. throw new IllegalArgumentException("placeCall: intent had no data");
  14. }
  15. String scheme = uri.getScheme();
  16. String number = PhoneNumberUtils.getNumberFromIntent(intent, mApp);
  17. if (VDBG) {
  18. log("- action: " + action);
  19. log("- uri: " + uri);
  20. log("- scheme: " + scheme);
  21. log("- number: " + number);
  22. }
  23. if (!(Intent.ACTION_CALL.equals(action)
  24. || Intent.ACTION_CALL_EMERGENCY.equals(action)
  25. || Intent.ACTION_CALL_PRIVILEGED.equals(action))) {
  26. Log.wtf(TAG, "placeCall: unexpected intent action " + action);
  27. throw new IllegalArgumentException("Unexpected action: " + action);
  28. }
  29. // Check to see if this is an OTASP call (the "activation" call
  30. // used to provision CDMA devices), and if so, do some
  31. // OTASP-specific setup.
  32. Phone phone = mApp.mCM.getDefaultPhone();
  33. if (TelephonyCapabilities.supportsOtasp(phone)) {
  34. checkForOtaspCall(intent);
  35. }
  36. mApp.setRestoreMuteOnInCallResume(false);
  37. // If a provider is used, extract the info to build the
  38. // overlay and route the call. The overlay will be
  39. // displayed when the InCallScreen becomes visible.
  40. if (PhoneUtils.hasPhoneProviderExtras(intent)) {
  41. inCallUiState.setProviderInfo(intent);
  42. } else {
  43. inCallUiState.clearProviderInfo();
  44. }
  45. //拨号
  46. CallStatusCode status = placeCallInternal(intent);
  47. switch (status) {
  48. case SUCCESS:
  49. case EXITED_ECM:
  50. if (DBG) log("==> placeCall(): success from placeCallInternal(): " + status);
  51. if (status == CallStatusCode.EXITED_ECM) {
  52. // Call succeeded, but we also need to tell the
  53. // InCallScreen to show the "Exiting ECM" warning.
  54. inCallUiState.setPendingCallStatusCode(CallStatusCode.EXITED_ECM);
  55. } else {
  56. // Call succeeded. There's no "error condition" that
  57. // needs to be displayed to the user, so clear out the
  58. // InCallUiState's "pending call status code".
  59. inCallUiState.clearPendingCallStatusCode();
  60. }
  61. // Notify the phone app that a call is beginning so it can
  62. // enable the proximity sensor
  63. mApp.setBeginningCall(true);
  64. break;
  65. default:
  66. // Any other status code is a failure.
  67. log("==> placeCall(): failure code from placeCallInternal(): " + status);
  68. // Handle the various error conditions that can occur when
  69. // initiating an outgoing call, typically by directing the
  70. // InCallScreen to display a diagnostic message (via the
  71. // "pending call status code" flag.)
  72. handleOutgoingCallError(status);
  73. break;
  74. }
  75. mApp.displayCallScreen();
  76. }
该函数首先得到拨打的电话号码及默认的Phone对象,调用placeCallInternal发起拨号请求,同时启动电话呼叫界面InCallScreen。

1.拨号流程

  1. private CallStatusCode placeCallInternal(Intent intent) {
  2. final InCallUiState inCallUiState = mApp.inCallUiState;
  3. final Uri uri = intent.getData();
  4. final String scheme = (uri != null) ? uri.getScheme() : null;
  5. String number;
  6. Phone phone = null;
  7. CallStatusCode okToCallStatus = checkIfOkToInitiateOutgoingCall(
  8. mCM.getServiceState());
  9. try {
  10. number = PhoneUtils.getInitialNumber(intent);
  11. if (VDBG) log("- actual number to dial: '" + number + "'");
  12. String sipPhoneUri = intent.getStringExtra(OutgoingCallBroadcaster.EXTRA_SIP_PHONE_URI);
  13. phone = PhoneUtils.pickPhoneBasedOnNumber(mCM, scheme, number, sipPhoneUri);
  14. if (VDBG) log("- got Phone instance: " + phone + ", class = " + phone.getClass());
  15. okToCallStatus = checkIfOkToInitiateOutgoingCall(phone.getServiceState().getState());
  16. } catch (PhoneUtils.VoiceMailNumberMissingException ex) {
  17. if (okToCallStatus != CallStatusCode.SUCCESS) {
  18. if (DBG) log("Voicemail number not reachable in current SIM card state.");
  19. return okToCallStatus;
  20. }
  21. if (DBG) log("VoiceMailNumberMissingException from getInitialNumber()");
  22. return CallStatusCode.VOICEMAIL_NUMBER_MISSING;
  23. }
  24. if (number == null) {
  25. Log.w(TAG, "placeCall: couldn't get a phone number from Intent " + intent);
  26. return CallStatusCode.NO_PHONE_NUMBER_SUPPLIED;
  27. }
  28. boolean isEmergencyNumber = PhoneNumberUtils.isLocalEmergencyNumber(number, mApp);
  29. boolean isPotentialEmergencyNumber =PhoneNumberUtils.isPotentialLocalEmergencyNumber(number, mApp);
  30. boolean isEmergencyIntent = Intent.ACTION_CALL_EMERGENCY.equals(intent.getAction());
  31. if (isPotentialEmergencyNumber && !isEmergencyIntent) {
  32. Log.e(TAG, "Non-CALL_EMERGENCY Intent " + intent+ " attempted to call potential emergency number " + number + ".");
  33. return CallStatusCode.CALL_FAILED;
  34. } else if (!isPotentialEmergencyNumber && isEmergencyIntent) {
  35. Log.e(TAG, "Received CALL_EMERGENCY Intent " + intent
  36. + " with non-potential-emergency number " + number
  37. + " -- failing call.");
  38. return CallStatusCode.CALL_FAILED;
  39. }
  40. // If we're trying to call an emergency number, then it's OK to
  41. // proceed in certain states where we'd otherwise bring up
  42. // an error dialog:
  43. // - If we're in EMERGENCY_ONLY mode, then (obviously) you're allowed
  44. // to dial emergency numbers.
  45. // - If we're OUT_OF_SERVICE, we still attempt to make a call,
  46. // since the radio will register to any available network.
  47. if (isEmergencyNumber
  48. && ((okToCallStatus == CallStatusCode.EMERGENCY_ONLY)
  49. || (okToCallStatus == CallStatusCode.OUT_OF_SERVICE))) {
  50. if (DBG) log("placeCall: Emergency number detected with status = " + okToCallStatus);
  51. okToCallStatus = CallStatusCode.SUCCESS;
  52. if (DBG) log("==> UPDATING status to: " + okToCallStatus);
  53. }
  54. if (okToCallStatus != CallStatusCode.SUCCESS) {
  55. // If this is an emergency call, launch the EmergencyCallHelperService
  56. // to turn on the radio and retry the call.
  57. if (isEmergencyNumber && (okToCallStatus == CallStatusCode.POWER_OFF)) {
  58. Log.i(TAG, "placeCall: Trying to make emergency call while POWER_OFF!");
  59. // If needed, lazily instantiate an EmergencyCallHelper instance.
  60. synchronized (this) {
  61. if (mEmergencyCallHelper == null) {
  62. mEmergencyCallHelper = new EmergencyCallHelper(this);
  63. }
  64. }
  65. // ...and kick off the "emergency call from airplane mode" sequence.
  66. mEmergencyCallHelper.startEmergencyCallFromAirplaneModeSequence(number);
  67. return CallStatusCode.SUCCESS;
  68. } else {
  69. if (DBG) log("==> placeCallInternal(): non-success status: " + okToCallStatus);
  70. return okToCallStatus;
  71. }
  72. }
  73. // Ok, we can proceed with this outgoing call.
  74. inCallUiState.needToShowCallLostDialog = false;
  75. inCallUiState.clearProgressIndication();
  76. Uri contactUri = intent.getData();
  77. //真正的电话拨号过程
  78. int callStatus = PhoneUtils.placeCall(mApp,
  79. phone,
  80. number,
  81. contactUri,
  82. (isEmergencyNumber || isEmergencyIntent),
  83. inCallUiState.providerGatewayUri);
  84. switch (callStatus) {
  85. case PhoneUtils.CALL_STATUS_DIALED:
  86. if (VDBG) log("placeCall: PhoneUtils.placeCall() succeeded for regular call '"
  87. + number + "'.");
  88. if (VDBG) log ("- inCallUiState.inCallScreenMode = "
  89. + inCallUiState.inCallScreenMode);
  90. if (inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL) {
  91. if (VDBG) log ("==> OTA_NORMAL note: switching to OTA_STATUS_LISTENING.");
  92. mApp.cdmaOtaScreenState.otaScreenState =
  93. CdmaOtaScreenState.OtaScreenState.OTA_STATUS_LISTENING;
  94. }
  95. boolean voicemailUriSpecified = scheme != null && scheme.equals("voicemail");
  96. // When voicemail is requested most likely the user wants to open
  97. // dialpad immediately, so we show it in the first place.
  98. // Otherwise we want to make sure the user can see the regular
  99. // in-call UI while the new call is dialing, and when it
  100. // first gets connected.)
  101. inCallUiState.showDialpad = voicemailUriSpecified;
  102. // For voicemails, we add context text to let the user know they
  103. // are dialing their voicemail.
  104. // TODO: This is only set here and becomes problematic when swapping calls
  105. inCallUiState.dialpadContextText = voicemailUriSpecified ?
  106. phone.getVoiceMailAlphaTag() : "";
  107. // Also, in case a previous call was already active (i.e. if
  108. // we just did "Add call"), clear out the "history" of DTMF
  109. // digits you typed, to make sure it doesn't persist from the
  110. // previous call to the new call.
  111. // TODO: it would be more precise to do this when the actual
  112. // phone state change happens (i.e. when a new foreground
  113. // call appears and the previous call moves to the
  114. // background), but the InCallScreen doesn't keep enough
  115. // state right now to notice that specific transition in
  116. // onPhoneStateChanged().
  117. inCallUiState.dialpadDigits = null;
  118. // Check for an obscure ECM-related scenario: If the phone
  119. // is currently in ECM (Emergency callback mode) and we
  120. // dial a non-emergency number, that automatically
  121. // *cancels* ECM. So warn the user about it.
  122. // (See InCallScreen.showExitingECMDialog() for more info.)
  123. boolean exitedEcm = false;
  124. if (PhoneUtils.isPhoneInEcm(phone) && !isEmergencyNumber) {
  125. Log.i(TAG, "About to exit ECM because of an outgoing non-emergency call");
  126. exitedEcm = true; // this will cause us to return EXITED_ECM from this method
  127. }
  128. if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
  129. // Start the timer for 3 Way CallerInfo
  130. if (mApp.cdmaPhoneCallState.getCurrentCallState()
  131. == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) {
  132. //Unmute for the second MO call
  133. PhoneUtils.setMute(false);
  134. // This is a "CDMA 3-way call", which means that you're dialing a
  135. // 2nd outgoing call while a previous call is already in progress.
  136. //
  137. // Due to the limitations of CDMA this call doesn't actually go
  138. // through the DIALING/ALERTING states, so we can't tell for sure
  139. // when (or if) it's actually answered. But we want to show
  140. // *some* indication of what's going on in the UI, so we "fake it"
  141. // by displaying the "Dialing" state for 3 seconds.
  142. // Set the mThreeWayCallOrigStateDialing state to true
  143. mApp.cdmaPhoneCallState.setThreeWayCallOrigState(true);
  144. // Schedule the "Dialing" indication to be taken down in 3 seconds:
  145. sendEmptyMessageDelayed(THREEWAY_CALLERINFO_DISPLAY_DONE,
  146. THREEWAY_CALLERINFO_DISPLAY_TIME);
  147. }
  148. }
  149. // Success!
  150. if (exitedEcm) {
  151. return CallStatusCode.EXITED_ECM;
  152. } else {
  153. return CallStatusCode.SUCCESS;
  154. }
  155. case PhoneUtils.CALL_STATUS_DIALED_MMI:
  156. if (DBG) log("placeCall: specified number was an MMI code: '" + number + "'.");
  157. // The passed-in number was an MMI code, not a regular phone number!
  158. // This isn't really a failure; the Dialer may have deliberately
  159. // fired an ACTION_CALL intent to dial an MMI code, like for a
  160. // USSD call.
  161. //
  162. // Presumably an MMI_INITIATE message will come in shortly
  163. // (and we'll bring up the "MMI Started" dialog), or else
  164. // an MMI_COMPLETE will come in (which will take us to a
  165. // different Activity; see PhoneUtils.displayMMIComplete()).
  166. return CallStatusCode.DIALED_MMI;
  167. case PhoneUtils.CALL_STATUS_FAILED:
  168. Log.w(TAG, "placeCall: PhoneUtils.placeCall() FAILED for number '"
  169. + number + "'.");
  170. // We couldn't successfully place the call; there was some
  171. // failure in the telephony layer.
  172. return CallStatusCode.CALL_FAILED;
  173. default:
  174. Log.wtf(TAG, "placeCall: unknown callStatus " + callStatus
  175. + " from PhoneUtils.placeCall() for number '" + number + "'.");
  176. return CallStatusCode.SUCCESS; // Try to continue anyway...
  177. }
  178. }
该函数通过调用PhoneUtils类的placeCall函数进入Framework层异步完成电话呼叫

src\com\android\phone\PhoneUtils.java

  1. public static int placeCall(Context context, Phone phone,
  2. String number, Uri contactRef, boolean isEmergencyCall,
  3. Uri gatewayUri) {
  4. final PhoneGlobals app = PhoneGlobals.getInstance();
  5. boolean useGateway = false;
  6. if (null != gatewayUri &&
  7. !isEmergencyCall &&
  8. PhoneUtils.isRoutableViaGateway(number)) { // Filter out MMI, OTA and other codes.
  9. useGateway = true;
  10. }
  11. int status = CALL_STATUS_DIALED;
  12. Connection connection;
  13. String numberToDial;
  14. if (useGateway) {
  15. if (null == gatewayUri || !Constants.SCHEME_TEL.equals(gatewayUri.getScheme())) {
  16. Log.e(LOG_TAG, "Unsupported URL:" + gatewayUri);
  17. return CALL_STATUS_FAILED;
  18. }
  19. // We can use getSchemeSpecificPart because we don't allow #
  20. // in the gateway numbers (treated a fragment delim.) However
  21. // if we allow more complex gateway numbers sequence (with
  22. // passwords or whatnot) that use #, this may break.
  23. // TODO: Need to support MMI codes.
  24. numberToDial = gatewayUri.getSchemeSpecificPart();
  25. } else {
  26. numberToDial = number;
  27. }
  28. // Remember if the phone state was in IDLE state before this call.
  29. // After calling CallManager#dial(), getState() will return different state.
  30. final boolean initiallyIdle = app.mCM.getState() == PhoneConstants.State.IDLE;
  31. try {
  32. connection = app.mCM.dial(phone, numberToDial);
  33. } catch (CallStateException ex) {
  34. // CallStateException means a new outgoing call is not currently
  35. // possible: either no more call slots exist, or there's another
  36. // call already in the process of dialing or ringing.
  37. Log.w(LOG_TAG, "Exception from app.mCM.dial()", ex);
  38. return CALL_STATUS_FAILED;
  39. // Note that it's possible for CallManager.dial() to return
  40. // null *without* throwing an exception; that indicates that
  41. // we dialed an MMI (see below).
  42. }
  43. int phoneType = phone.getPhoneType();
  44. // On GSM phones, null is returned for MMI codes
  45. if (null == connection) {
  46. if (phoneType == PhoneConstants.PHONE_TYPE_GSM && gatewayUri == null) {
  47. if (DBG) log("dialed MMI code: " + number);
  48. status = CALL_STATUS_DIALED_MMI;
  49. } else {
  50. status = CALL_STATUS_FAILED;
  51. }
  52. } else {
  53. if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
  54. updateCdmaCallStateOnNewOutgoingCall(app);
  55. }
  56. // Clean up the number to be displayed.
  57. if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
  58. number = CdmaConnection.formatDialString(number);
  59. }
  60. number = PhoneNumberUtils.extractNetworkPortion(number);
  61. number = PhoneNumberUtils.convertKeypadLettersToDigits(number);
  62. number = PhoneNumberUtils.formatNumber(number);
  63. if (gatewayUri == null) {
  64. // phone.dial() succeeded: we're now in a normal phone call.
  65. // attach the URI to the CallerInfo Object if it is there,
  66. // otherwise just attach the Uri Reference.
  67. // if the uri does not have a "content" scheme, then we treat
  68. // it as if it does NOT have a unique reference.
  69. String content = context.getContentResolver().SCHEME_CONTENT;
  70. if ((contactRef != null) && (contactRef.getScheme().equals(content))) {
  71. Object userDataObject = connection.getUserData();
  72. if (userDataObject == null) {
  73. connection.setUserData(contactRef);
  74. } else {
  75. // TODO: This branch is dead code, we have
  76. // just created the connection which has
  77. // no user data (null) by default.
  78. if (userDataObject instanceof CallerInfo) {
  79. ((CallerInfo) userDataObject).contactRefUri = contactRef;
  80. } else {
  81. ((CallerInfoToken) userDataObject).currentInfo.contactRefUri =
  82. contactRef;
  83. }
  84. }
  85. }
  86. } else {
  87. // Get the caller info synchronously because we need the final
  88. // CallerInfo object to update the dialed number with the one
  89. // requested by the user (and not the provider's gateway number).
  90. CallerInfo info = null;
  91. String content = phone.getContext().getContentResolver().SCHEME_CONTENT;
  92. if ((contactRef != null) && (contactRef.getScheme().equals(content))) {
  93. info = CallerInfo.getCallerInfo(context, contactRef);
  94. }
  95. // Fallback, lookup contact using the phone number if the
  96. // contact's URI scheme was not content:// or if is was but
  97. // the lookup failed.
  98. if (null == info) {
  99. info = CallerInfo.getCallerInfo(context, number);
  100. }
  101. info.phoneNumber = number;
  102. connection.setUserData(info);
  103. }
  104. setAudioMode();
  105. if (DBG) log("about to activate speaker");
  106. // Check is phone in any dock, and turn on speaker accordingly
  107. final boolean speakerActivated = activateSpeakerIfDocked(phone);
  108. // See also similar logic in answerCall().
  109. if (initiallyIdle && !speakerActivated && isSpeakerOn(app)
  110. && !app.isBluetoothHeadsetAudioOn()) {
  111. // This is not an error but might cause users' confusion. Add log just in case.
  112. Log.i(LOG_TAG, "Forcing speaker off when initiating a new outgoing call...");
  113. PhoneUtils.turnOnSpeaker(app, false, true);
  114. }
  115. }
  116. return status;
  117. }
该函数调用framework层的CallManager的dial函数。

  1. public Connection dial(Phone phone, String dialString) throws CallStateException {
  2. Phone basePhone = getPhoneBase(phone);
  3. Connection result;
  4. if (VDBG) {
  5. Log.d(LOG_TAG, " dial(" + basePhone + ", "+ dialString + ")");
  6. Log.d(LOG_TAG, this.toString());
  7. }
  8. if (!canDial(phone)) {
  9. throw new CallStateException("cannot dial in current state");
  10. }
  11. if (hasActiveFgCall() ) {
  12. Phone activePhone = getActiveFgCall().getPhone();
  13. boolean hasBgCall = !(activePhone.getBackgroundCall().isIdle());
  14. if (DBG) {
  15. Log.d(LOG_TAG, "hasBgCall: "+ hasBgCall + " sameChannel:" + (activePhone == basePhone));
  16. }
  17. if (activePhone != basePhone) {
  18. if (hasBgCall) {
  19. Log.d(LOG_TAG, "Hangup");
  20. getActiveFgCall().hangup();
  21. } else {
  22. Log.d(LOG_TAG, "Switch");
  23. activePhone.switchHoldingAndActive();
  24. }
  25. }
  26. }
  27. result = basePhone.dial(dialString);
  28. if (VDBG) {
  29. Log.d(LOG_TAG, "End dial(" + basePhone + ", "+ dialString + ")");
  30. Log.d(LOG_TAG, this.toString());
  31. }
  32. return result;
  33. }
函数首先取得相应类型的Phone,并判断该Phone的状态,这里得到PhoneProxy类型的Phone,PhoneProxy是所有类型Phone的代理类,在构造PhoneProxy时,把对应类型的Phone保存在其成员变量mActivePhone中,有关Phone,PhoneProxy,GmsPhone,CDMAPhone之间的关系请参看 Android电话Phone设计框架介绍,GSM类型的网络对应GSMPhone,因此这里将调用GSMPhone类的dial函数。

./telephony/java/com/android/internal/telephony/gsm/GSMPhone.java

  1. public Connection dial(String dialString) throws CallStateException {
  2. return dial(dialString, null);
  3. }

  1. public Connection dial (String dialString, UUSInfo uusInfo) throws CallStateException {
  2. // Need to make sure dialString gets parsed properly
  3. String newDialString = PhoneNumberUtils.stripSeparators(dialString);
  4. // handle in-call MMI first if applicable
  5. if (handleInCallMmiCommands(newDialString)) {
  6. return null;
  7. }
  8. // Only look at the Network portion for mmi
  9. String networkPortion = PhoneNumberUtils.extractNetworkPortionAlt(newDialString);
  10. GsmMmiCode mmi = GsmMmiCode.newFromDialString(networkPortion, this);
  11. if (mmi == null) {
  12. return mCT.dial(newDialString, uusInfo);
  13. } else if (mmi.isTemporaryModeCLIR()) {
  14. return mCT.dial(mmi.dialingNumber, mmi.getCLIRMode(), uusInfo);
  15. } else {
  16. mPendingMMIs.add(mmi);
  17. mMmiRegistrants.notifyRegistrants(new AsyncResult(null, mmi, null));
  18. mmi.processCode();
  19. // FIXME should this return null or something else?
  20. return null;
  21. }
  22. }
GSMPhone又通过CallTracker来向RIL发送请求,关于CallTracker的类关系请参阅 Android电话Phone设计框架介绍

./telephony/java/com/android/internal/telephony/gsm/GsmCallTracker.java

  1. synchronized Connection dial (String dialString, int clirMode, UUSInfo uusInfo) throws CallStateException {
  2. // note that this triggers call state changed notif
  3. clearDisconnected();
  4. if (!canDial()) {
  5. throw new CallStateException("cannot dial in current state");
  6. }
  7. // The new call must be assigned to the foreground call.
  8. // That call must be idle, so place anything that's
  9. // there on hold
  10. if (foregroundCall.getState() == GsmCall.State.ACTIVE) {
  11. // this will probably be done by the radio anyway
  12. // but the dial might fail before this happens
  13. // and we need to make sure the foreground call is clear
  14. // for the newly dialed connection
  15. switchWaitingOrHoldingAndActive();
  16. // Fake local state so that
  17. // a) foregroundCall is empty for the newly dialed connection
  18. // b) hasNonHangupStateChanged remains false in the
  19. // next poll, so that we don't clear a failed dialing call
  20. fakeHoldForegroundBeforeDial();
  21. }
  22. if (foregroundCall.getState() != GsmCall.State.IDLE) {
  23. //we should have failed in !canDial() above before we get here
  24. throw new CallStateException("cannot dial in current state");
  25. }
  26. // pendingMO = new GsmConnection(phone.getContext(), checkForTestEmergencyNumber(dialString),
  27. // this, foregroundCall);
  28. boolean isStkCall = getStkCall();
  29. log("GsmCallTracker dial: isStkCall=" + isStkCall);
  30. pendingMO = new GsmConnection(phone.getContext(), dialString, this, foregroundCall, isStkCall, false);
  31. hangupPendingMO = false;
  32. if (pendingMO.address == null || pendingMO.address.length() == 0
  33. || pendingMO.address.indexOf(PhoneNumberUtils.WILD) >= 0
  34. ) {
  35. // Phone number is invalid
  36. pendingMO.cause = Connection.DisconnectCause.INVALID_NUMBER;
  37. // handlePollCalls() will notice this call not present
  38. // and will mark it as dropped.
  39. pollCallsWhenSafe();
  40. } else {
  41. // Always unmute when initiating a new call
  42. setMute(false);
  43. // cm.dial(pendingMO.address, clirMode, uusInfo, obtainCompleteMessage());
  44. // Add for bug 121825 Start
  45. String tmpAddr = pendingMO.address;
  46. if (PhoneNumberUtils.isCustomEmergencyNumber(pendingMO.address)) {
  47. Log.d(LOG_TAG,"Pending MO is Custom Emergency call");
  48. tmpAddr = tmpAddr + "/1";
  49. }
  50. //cm.dial(pendingMO.address, clirMode, uusInfo, isStkCall, obtainCompleteMessage());
  51. cm.dial(tmpAddr, clirMode, uusInfo, isStkCall, obtainCompleteMessage());
  52. // Add for bug 121825 End
  53. }
  54. updatePhoneState();
  55. phone.notifyPreciseCallStateChanged();
  56. return pendingMO;
  57. }
cm的类型为CommandsInterface,RIL.java实现了CommandsInterface接口,因此GsmCallTracker最终是通过RIL来发送拨号请求的。

./telephony/java/com/android/internal/telephony/RIL.java

  1. public void dial(String address, int clirMode, UUSInfo uusInfo, boolean isStkCall, Message result) {
  2. RILRequest rr;
  3. if (address.indexOf('/') == -1) {
  4. rr = RILRequest.obtain(RIL_REQUEST_DIAL, result);
  5. } else {
  6. rr = RILRequest.obtain(RIL_REQUEST_DIAL_EMERGENCY_CALL, result);
  7. }
  8. rr.mp.writeString(address);
  9. rr.mp.writeInt(clirMode);
  10. rr.mp.writeInt(0); // UUS information is absent
  11. if (uusInfo == null) {
  12. rr.mp.writeInt(0); // UUS information is absent
  13. } else {
  14. rr.mp.writeInt(1); // UUS information is present
  15. rr.mp.writeInt(uusInfo.getType());
  16. rr.mp.writeInt(uusInfo.getDcs());
  17. rr.mp.writeByteArray(uusInfo.getUserData());
  18. }
  19. rr.mp.writeInt(isStkCall ? 1:0);
  20. if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
  21. send(rr);
  22. }

Java部分的request请求号需要与C/C++部分的请求号保持一致。当需要执行某种AT命令request请求时,则需创建一个新的RILRequest,使用RILRequest的obtain函数,该obtain静态函数用于从其内部维护的一个 RIL request池sPool中取下一个request,得到一个RILRequest对象,它里面的请求号和用于回送结果及处理者handler的消息来自传递的实参。当一个RILRequest对象不再使用时,调用release() 函数将其释放回池中。将RILRequest请求放置到消息队列上,然后sender线程将其写入socket,rild侧通过dispatch线程将请求分发出去。在RIL类中,还维护了一个RILRequest请求列表,RILRequest类中的serial作为其id标识。当sender发送一个RIL请求后,则将其添加到该列表中,若发送时出现异常则需再清除;当请求完成并得到回送的response消息后,使用findAndRemoveRequestFromList函数将其移除。RIL请求执行AT是一个异步的过程:调用者调用RIL类的API函数只是往线程的消息队列上添加了一消息就返回;然后线程在执行无限循环时将其写到socket中,并将RILRequest对象添加到一个列表中;当RILReciever线程收到数据并解析,然后查询系列号后得到这是某个先前的RIL请求后,将AT执行的返回结果放到AsynResult中并赋值给Message中的obj成员后,由Message.sendToTarget送回到调用者并由其处理。


  1. protected void send(RILRequest rr) {
  2. Message msg;
  3. if (mSocket == null) {
  4. rr.onError(RADIO_NOT_AVAILABLE, null);
  5. rr.release();
  6. return;
  7. }
  8. msg = mSender.obtainMessage(EVENT_SEND, rr);
  9. acquireWakeLock();
  10. msg.sendToTarget();
  11. }
RILSender消息处理过程

  1. class RILSender extends Handler implements Runnable {
  2. public RILSender(Looper looper) {
  3. super(looper);
  4. }
  5. byte[] dataLength = new byte[4];
  6. public void
  7. run() {
  8. //setup if needed
  9. }
  10. @Override public void
  11. handleMessage(Message msg) {
  12. RILRequest rr = (RILRequest)(msg.obj);
  13. RILRequest req = null;
  14. switch (msg.what) {
  15. case EVENT_SEND:
  16. /**
  17. * mRequestMessagePending++ already happened for every
  18. * EVENT_SEND, thus we must make sure
  19. * mRequestMessagePending-- happens once and only once
  20. */
  21. boolean alreadySubtracted = false;
  22. try {
  23. LocalSocket s;
  24. s = mSocket;
  25. if (s == null) {
  26. rr.onError(RADIO_NOT_AVAILABLE, null);
  27. rr.release();
  28. if (mRequestMessagesPending > 0)
  29. mRequestMessagesPending--;
  30. alreadySubtracted = true;
  31. return;
  32. }
  33. synchronized (mRequestsList) {
  34. mRequestsList.add(rr);
  35. mRequestMessagesWaiting++;
  36. }
  37. if (mRequestMessagesPending > 0)
  38. mRequestMessagesPending--;
  39. alreadySubtracted = true;
  40. byte[] data;
  41. data = rr.mp.marshall();
  42. rr.mp.recycle();
  43. rr.mp = null;
  44. if (data.length > RIL_MAX_COMMAND_BYTES) {
  45. throw new RuntimeException("Parcel larger than max bytes allowed! "+ data.length);
  46. }
  47. // parcel length in big endian
  48. dataLength[0] = dataLength[1] = 0;
  49. dataLength[2] = (byte)((data.length >> 8) & 0xff);
  50. dataLength[3] = (byte)((data.length) & 0xff);
  51. //Log.v(LOG_TAG, "writing packet: " + data.length + " bytes");
  52. s.getOutputStream().write(dataLength);
  53. s.getOutputStream().write(data);
  54. } catch (IOException ex) {
  55. Log.e(LOG_TAG, "IOException", ex);
  56. req = findAndRemoveRequestFromList(rr.mSerial);
  57. // make sure this request has not already been handled,
  58. // eg, if RILReceiver cleared the list.
  59. if (req != null || !alreadySubtracted) {
  60. rr.onError(RADIO_NOT_AVAILABLE, null);
  61. rr.release();
  62. }
  63. } catch (RuntimeException exc) {
  64. Log.e(LOG_TAG, "Uncaught exception ", exc);
  65. req = findAndRemoveRequestFromList(rr.mSerial);
  66. // make sure this request has not already been handled,
  67. // eg, if RILReceiver cleared the list.
  68. if (req != null || !alreadySubtracted) {
  69. rr.onError(GENERIC_FAILURE, null);
  70. rr.release();
  71. }
  72. } finally {
  73. // Note: We are "Done" only if there are no outstanding
  74. // requests or replies. Thus this code path will only release
  75. // the wake lock on errors.
  76. releaseWakeLockIfDone();
  77. }
  78. if (!alreadySubtracted && mRequestMessagesPending > 0) {
  79. mRequestMessagesPending--;
  80. }
  81. break;
  82. }
  83. }
  84. }
在处理EVENT_SEND消息时,将请求参数写入到rild套接字中,在 Android电话Phone设计框架介绍中介绍了rild服务进程作为Android电话系统的服务端,接收客户端framework层发送过来的请求,并与modem交互,实现整个拨号过程。关于rild服务在 Android之rild进程启动源码分析已经详细介绍了,至此电话拨号请求的发送过程就完成了。拨号的本质就是应用层Phone进程首先对拨打的号码进行一系列处理,然后进入framework层,通过framework层的电话客户端发送线程将请求通过套接字的方式发送给电话服务进程rild,rild将该请求映射为相应的AT指令发送到modem中。


2.拨号界面显示

在CallController的placeCall函数中,首先将拨号请求发送到rild服务进程,然后启动呼叫界面InCallScreen,Android电话Phone UI分析对InCallScreen的UI布局进行了详细的分析,只有了解InCallScreen的UI布局,才能更好地理解InCallScreen的启动过程。InCallScreen主要是显示通话界面, 并且还负责菜单项各种按键事件和触摸时间的处理。同时本类还复写的finish()方法,所以一般不会被finish掉,调用该方法时它又把自己放回栈中。

src\com\android\phone\PhoneGlobals.java

  1. void displayCallScreen() {
  2. if (VDBG) Log.d(LOG_TAG, "displayCallScreen()...");
  3. // On non-voice-capable devices we shouldn't ever be trying to
  4. // bring up the InCallScreen in the first place.
  5. if (!sVoiceCapable) {
  6. Log.w(LOG_TAG, "displayCallScreen() not allowed: non-voice-capable device",new Throwable("stack dump"));
  7. return;
  8. }
  9. //启动电话呼叫界面InCallScreen
  10. try {
  11. startActivity(createInCallIntent());
  12. } catch (ActivityNotFoundException e) {
  13. Log.w(LOG_TAG, "displayCallScreen: transition to InCallScreen failed: " + e);
  14. }
  15. Profiler.callScreenRequested();
  16. }


  1. static Intent createInCallIntent() {
  2. Intent intent = new Intent(Intent.ACTION_MAIN, null);
  3. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
  4. | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
  5. | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
  6. intent.setClassName("com.android.phone", getCallScreenClassName());
  7. return intent;
  8. }
  9. private static String getCallScreenClassName() {
  10. return InCallScreen.class.getName();
  11. }

第一次启动InCallScreen,首先调用其onCreate函数

src\com\android\phone\InCallScreen.java
  1. protected void onCreate(Bundle icicle) {
  2. Log.i(LOG_TAG, "onCreate()... this = " + this);
  3. //获得通话界面被创建的时间
  4. Profiler.callScreenOnCreate();
  5. super.onCreate(icicle);
  6. // Make sure this is a voice-capable device.
  7. if (!PhoneGlobals.sVoiceCapable) {
  8. Log.wtf(LOG_TAG, "onCreate() reached on non-voice-capable device");
  9. finish();
  10. return;
  11. }
  12. mApp = PhoneGlobals.getInstance();
  13. mApp.setInCallScreenInstance(this);
  14. // set this flag so this activity will stay in front of the keyguard
  15. int flags = WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
  16. | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
  17. if (mApp.getPhoneState() == PhoneConstants.State.OFFHOOK) {
  18. flags |= WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
  19. }
  20. WindowManager.LayoutParams lp = getWindow().getAttributes();
  21. lp.flags |= flags;
  22. if (!mApp.proximitySensorModeEnabled()) {
  23. // If we don't have a proximity sensor, then the in-call screen explicitly
  24. // controls user activity. This is to prevent spurious touches from waking
  25. // the display.
  26. lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY;
  27. }
  28. //设置窗体属性
  29. getWindow().setAttributes(lp);
  30. setPhone(mApp.phone); // Sets mPhone
  31. mCM = mApp.mCM;
  32. log("- onCreate: phone state = " + mCM.getState());
  33. mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
  34. if (mBluetoothAdapter != null) {
  35. mBluetoothAdapter.getProfileProxy(getApplicationContext(), mBluetoothProfileServiceListener,
  36. BluetoothProfile.HEADSET);
  37. }
  38. //设置窗体显示风格
  39. requestWindowFeature(Window.FEATURE_NO_TITLE);
  40. //加载布局文件
  41. setContentView(R.layout.incall_screen);
  42. final ViewStub touchUiStub = (ViewStub) findViewById(
  43. mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA
  44. ? R.id.inCallTouchUiCdmaStub : R.id.inCallTouchUiStub);
  45. if (touchUiStub != null) touchUiStub.inflate();
  46. //加载各种view组建
  47. initInCallScreen();
  48. //对通话的各种状态进行广播。
  49. registerForPhoneStates();
  50. //判断是否使用了OTA技术,通过该判断设置通话界面的样式。
  51. if (icicle == null) {
  52. if (DBG) log("onCreate(): this is our very first launch, checking intent...");
  53. internalResolveIntent(getIntent());
  54. }
  55. //记录通话界面创建完成后的时间
  56. Profiler.callScreenCreated();
  57. if (DBG) log("onCreate(): exit");
  58. }
UI初始化过程,为了能更好的理解UI初始化过程,请查看 Android电话Phone UI分析一文了解电话呼叫界面的UI布局。

  1. private void initInCallScreen() {
  2. if (VDBG) log("initInCallScreen()...");
  3. // Have the WindowManager filter out touch events that are "too fat".
  4. getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
  5. // Initialize the CallCard.
  6. mCallCard = (CallCard) findViewById(R.id.callCard);
  7. if (VDBG) log(" - mCallCard = " + mCallCard);
  8. mCallCard.setInCallScreenInstance(this);
  9. // Initialize the onscreen UI elements.
  10. initInCallTouchUi();
  11. // Helper class to keep track of enabledness/state of UI controls
  12. mInCallControlState = new InCallControlState(this, mCM);
  13. // Helper class to run the "Manage conference" UI
  14. mManageConferenceUtils = new ManageConferenceUtils(this, mCM);
  15. // The DTMF Dialpad.
  16. ViewStub stub = (ViewStub) findViewById(R.id.dtmf_twelve_key_dialer_stub);
  17. mDialer = new DTMFTwelveKeyDialer(this, stub);
  18. mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
  19. }

  1. private void internalResolveIntent(Intent intent) {
  2. if (intent == null || intent.getAction() == null) {
  3. return;
  4. }
  5. String action = intent.getAction();
  6. if (DBG) log("internalResolveIntent: action=" + action);
  7. if (action.equals(intent.ACTION_MAIN)) {
  8. if (intent.hasExtra(SHOW_DIALPAD_EXTRA)) {
  9. boolean showDialpad = intent.getBooleanExtra(SHOW_DIALPAD_EXTRA, false);
  10. if (VDBG) log("- internalResolveIntent: SHOW_DIALPAD_EXTRA: " + showDialpad);
  11. mApp.inCallUiState.showDialpad = showDialpad;
  12. final boolean hasActiveCall = mCM.hasActiveFgCall();
  13. final boolean hasHoldingCall = mCM.hasActiveBgCall();
  14. if (showDialpad && !hasActiveCall && hasHoldingCall) {
  15. PhoneUtils.switchHoldingAndActive(mCM.getFirstActiveBgCall());
  16. }
  17. }
  18. return;
  19. }
  20. if (action.equals(OtaUtils.ACTION_DISPLAY_ACTIVATION_SCREEN)) {
  21. if (!TelephonyCapabilities.supportsOtasp(mPhone)) {
  22. throw new IllegalStateException(
  23. "Received ACTION_DISPLAY_ACTIVATION_SCREEN intent on non-OTASP-capable device: "
  24. + intent);
  25. }
  26. setInCallScreenMode(InCallScreenMode.OTA_NORMAL);
  27. if ((mApp.cdmaOtaProvisionData != null)
  28. && (!mApp.cdmaOtaProvisionData.isOtaCallIntentProcessed)) {
  29. mApp.cdmaOtaProvisionData.isOtaCallIntentProcessed = true;
  30. mApp.cdmaOtaScreenState.otaScreenState =
  31. CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION;
  32. }
  33. return;
  34. }
  35. if (action.equals(OtaUtils.ACTION_PERFORM_CDMA_PROVISIONING)) {
  36. throw new IllegalStateException(
  37. "Unexpected ACTION_PERFORM_CDMA_PROVISIONING received by InCallScreen: "+ intent);
  38. } else if (action.equals(Intent.ACTION_CALL) || action.equals(Intent.ACTION_CALL_EMERGENCY)) {
  39. // ACTION_CALL* intents go to the OutgoingCallBroadcaster, which now
  40. // translates them into CallController.placeCall() calls rather than
  41. // launching the InCallScreen directly.
  42. throw new IllegalStateException("Unexpected CALL action received by InCallScreen: "+ intent);
  43. } else if (action.equals(ACTION_UNDEFINED)) {
  44. // This action is only used for internal bookkeeping; we should
  45. // never actually get launched with it.
  46. Log.wtf(LOG_TAG, "internalResolveIntent: got launched with ACTION_UNDEFINED");
  47. return;
  48. } else {
  49. Log.wtf(LOG_TAG, "internalResolveIntent: unexpected intent action: " + action);
  50. // But continue the best we can (basically treating this case
  51. // like ACTION_MAIN...)
  52. return;
  53. }
  54. }


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/代码探险家/article/detail/988289
推荐阅读
相关标签
  

闽ICP备14008679号