建構預設手機應用程式

預設手機應用程式可讓 Android Telecom 架構使用角色管理員和通話中服務,為 Android 裝置上的預設手機應用程式建立替代電話,藉此告知應用程式通話狀態,進而實作 InCallService API。導入結果必須符合下列條件:

其不能有任何呼叫功能,而且僅包含要呼叫的使用者介面。它必須處理電信架構知道的所有呼叫,亦不得對呼叫的性質做出假設。例如,不得假設通話是透過 SIM 卡撥打的電話,也不得實作基於任何 ConnectionService 的通話限制,例如強制執行視訊通話的電話通訊限制。

通話應用程式可讓使用者在裝置上接收或撥打音訊或視訊通話。呼叫應用程式會使用自己的使用者介面進行通話,而非使用預設的「電話」應用程式介面,如以下螢幕截圖所示。

通話應用程式範例
使用本身的使用者介面呼叫應用程式的範例

Android 架構包含 android.telecom 套件,其中包含的類別可協助您根據電信架構建構呼叫應用程式。根據電信架構建構應用程式可提供下列優點:

  • 您的應用程式能正確與裝置上的原生電信子系統互通。
  • 您的應用程式可正確與其他同樣遵循架構的呼叫應用程式互通。
  • 這個架構可協助應用程式管理音訊和視訊轉送。
  • 此架構可協助應用程式判斷呼叫是否聚焦。

資訊清單宣告和權限

在應用程式資訊清單中,宣告應用程式使用 MANAGE_OWN_CALLS 權限,如以下範例所示:

<manifest … >
    <uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
</manifest>

如要進一步瞭解如何宣告應用程式權限,請參閱「權限」一文。

您必須宣告一項服務,用於指定在應用程式中實作 ConnectionService 類別的類別。電信子系統要求服務宣告 BIND_TELECOM_CONNECTION_SERVICE 權限,才能繫結至該類別。以下範例說明如何在應用程式資訊清單中宣告服務:

<service android:name="com.example.MyConnectionService"
    android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
    <intent-filter>
        <action android:name="android.telecom.ConnectionService" />
    </intent-filter>
</service>

如要進一步瞭解如何宣告應用程式元件 (包括服務),請參閱「應用程式元件」。

實作連線服務

呼叫應用程式必須提供 ConnectionService 類別實作,電信子系統可繫結在一起。您的 ConnectionService 實作應覆寫下列方法:

onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)

電信子系統會呼叫這個方法,以回應應用程式呼叫 placeCall(Uri, Bundle) 以建立新的傳出呼叫。應用程式會傳回新的 Connection 類別實作例項 (詳情請參閱「實作連線」),代表新的撥出呼叫。您可以執行下列動作,進一步自訂傳出連線:

onCreateOutgoingConnectionFailed(PhoneAccountHandle, ConnectionRequest)

當應用程式呼叫 placeCall(Uri, Bundle) 方法,導致撥出電話時,電信子系統會呼叫這個方法。為因應這種情況,應用程式應通知使用者 (例如使用快訊方塊或浮動式訊息) 無法撥出電話。如果緊急電話正在進行,或是另一個應用程式正在進行通話,但無法在撥打電話前將通話保留,則應用程式可能無法撥打電話。

onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest)

當應用程式呼叫 addNewIncomingCall(PhoneAccountHandle, Bundle) 方法,向系統通知應用程式中有新的來電時,電信子系統會呼叫這個方法。應用程式會傳回 Connection 實作的新例項 (詳情請參閱「實作連線」以代表新的來電)。您可以執行下列動作,進一步自訂連入連線:

onCreateIncomingConnectionFailed(PhoneAccountHandle, ConnectionRequest)

當應用程式呼叫 addNewIncomingCall(PhoneAccountHandle, Bundle) 方法向 Telecom 告知新的來電,但系統不允許來電時,電信子系統會呼叫這個方法 (詳情請參閱「呼叫限制」)。應用程式應無聲拒絕來電,可選擇是否發布通知,通知使用者未接來電。

實作連線

應用程式應建立 Connection 的子類別,代表應用程式中的呼叫。您應在實作中覆寫下列方法:

onShowIncomingCallUi()

當您新增來電時,電信子系統會呼叫這個方法,而應用程式應顯示來電 UI。

onCallAudioStateChanged(CallAudioState)

電信子系統會呼叫此方法,通知應用程式目前的音訊路徑或模式已變更。為回應應用程式使用 setAudioRoute(int) 方法變更音訊模式的情況,系統會呼叫此方法。如果系統變更音訊路徑 (例如藍牙耳機中斷連線時),也可以呼叫這個方法。

onHold()

當電信子系統想保留通話時,會呼叫此方法。為回應這項要求,應用程式應保留呼叫,並叫用 setOnHold() 方法,通知系統保留呼叫。當呼叫中的服務 (例如 Android Auto) 要轉發使用者要求,以便保留通話時,電信子系統可能會呼叫此方法。如果使用者在其他應用程式中發出呼叫,電信子系統也會呼叫這個方法。如要進一步瞭解通話服務,請參閱 InCallService

onUnhold()

電信子系統在想恢復已保留的呼叫時,會呼叫這個方法。應用程式恢復呼叫後,應叫用 setActive() 方法,通知系統通話已解除。當呼叫中的服務 (例如 Android Auto) 要轉發要求來恢復通話時,電信子系統可能會呼叫這個方法。如要進一步瞭解通話服務,請參閱 InCallService

onAnswer()

電信子系統會呼叫此方法,通知應用程式應接聽來電。應用程式接受呼叫後,應叫用 setActive() 方法,通知系統已回應呼叫。當應用程式新增來電,且另一個應用程式中正在進行無法保留的通話時,電信子系統可能會呼叫這個方法。電信子系統在這些執行個體中會代表您的應用程式顯示來電 UI。此架構提供超載方法,可支援指定應接聽來電的影片狀態。詳情請參閱 onAnswer(int) 的說明。

onReject()

當電信子系統想拒絕來電時,會呼叫這個方法。應用程式拒絕呼叫後,應呼叫 setDisconnected(DisconnectCause),並將 REJECTED 指定為參數。接著,應用程式應呼叫 destroy() 方法,通知系統應用程式已處理呼叫。使用者拒絕來自應用程式的來電時,電信子系統會呼叫這個方法。

onDisconnect()

電信子系統在想中斷通話時會呼叫這個方法。呼叫結束後,應用程式應呼叫 setDisconnected(DisconnectCause) 方法,並將 LOCAL 指定為參數,表示使用者要求導致呼叫中斷連線。接著,應用程式應呼叫 destroy() 方法,通知電信子系統應用程式已處理呼叫。當使用者透過其他通話服務 (例如 Android Auto) 中斷通話時,系統可能會呼叫這個方法。當您的呼叫必須中斷連線以允許撥打其他電話,例如使用者想要撥打緊急電話時,系統也會呼叫此方法。如要進一步瞭解通話服務,請參閱 InCallService

處理常見的通話情境

在呼叫流程中使用 ConnectionService API,涉及與 android.telecom 套件中的其他類別互動。以下各節將說明常見的呼叫情境,以及應用程式應如何使用 API 處理這些情境。

接聽來電

無論其他應用程式是否有呼叫,處理來電的流程都會改變。各流程的不同原因在於,電信架構必須在其他應用程式有有效呼叫時建立一些限制,以確保裝置上所有呼叫的應用程式都有穩定的環境。詳情請參閱「呼叫限制」。

其他應用程式中沒有進行中的通話

如要在其他應用程式沒有進行中的通話時接聽來電,請按照下列步驟操作:

  1. 您的應用程式會透過一般機制接收新的來電。
  2. 使用 addNewIncomingCall(PhoneAccountHandle, Bundle) 方法將新來電通知電信子系統。
  3. 電信子系統會繫結至應用程式的 ConnectionService 實作,並使用 onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest) 方法要求 Connection 類別的新執行個體,代表新的傳入呼叫。
  4. 電信子系統通知應用程式應使用 onShowIncomingCallUi() 方法顯示來電使用者介面。
  5. 應用程式會使用含有關聯全螢幕意圖的通知,顯示傳入的 UI。詳情請參閱 onShowIncomingCallUi()
  6. 如果使用者接受來電,請呼叫 setActive() 方法;或 setDisconnected(DisconnectCause) 指定 REJECTED 做為參數,然後在使用者拒絕來電時呼叫 destroy() 方法。

其他應用程式正在進行無法保留的通話

如要在其他應用程式進行無法保留的通話時接聽來電,請按照下列步驟操作:

  1. 您的應用程式會透過一般機制接收新的來電。
  2. 使用 addNewIncomingCall(PhoneAccountHandle, Bundle) 方法將新來電通知電信子系統。
  3. 電信子系統會繫結至應用程式的 ConnectionService 實作,並使用 onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest) 方法要求新的 Connection 物件例項,代表新來電。
  4. 電信子系統會顯示來電的來電 UI。
  5. 如果使用者接聽來電,電信子系統會呼叫 onAnswer() 方法。應呼叫 setActive() 方法,向電話子系統指出呼叫已連線。
  6. 如果使用者拒絕通話,電信子系統會呼叫 onReject() 方法。您應呼叫 setDisconnected(DisconnectCause) 方法,將 REJECTED 指定為參數,然後呼叫 destroy() 方法。

撥出電話

撥打撥出呼叫的流程會處理因電信架構設下的限制,而無法呼叫呼叫的可能性。詳情請參閱「呼叫限制」。

如要撥打電話,請按照下列步驟操作:

  1. 使用者在應用程式中撥出電話。
  2. 使用 placeCall(Uri, Bundle) 方法,通知電信子系統新的撥出電話。請留意方法參數的注意事項:
    • Uri 參數代表呼叫的所在地址。如果是一般電話號碼,請使用 tel: URI 配置。
    • Bundle 參數可讓您將應用程式的 PhoneAccountHandle 物件新增至 EXTRA_PHONE_ACCOUNT_HANDLE 額外項目,以提供呼叫應用程式的相關資訊。應用程式必須為每個撥出呼叫提供 PhoneAccountHandle 物件。
    • 透過 Bundle 參數,您也可以在 EXTRA_START_CALL_WITH_VIDEO_STATE 額外項目中指定 STATE_BIDIRECTIONAL 值,藉此指明撥出電話是否包含影片。在此情況下,電信子系統預設會將視訊通話傳送至喇叭。
  3. 電信子系統會繫結至應用程式的 ConnectionService 實作。
  4. 如果應用程式無法撥出電話,電信子系統會呼叫 onCreateOutgoingConnectionFailed(PhoneAccountHandle, ConnectionRequest) 方法,通知應用程式目前無法撥打電話。應用程式應告知使用者無法撥打電話。
  5. 如果應用程式能夠撥打電話,電信子系統會呼叫 onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest) 方法。應用程式應傳回 Connection 類別的執行個體,代表新的撥出呼叫。如要進一步瞭解應在連線中設定的屬性,請參閱「實作連線服務」。
  6. 撥出電話後,請呼叫 setActive() 方法,通知電信子系統正在進行通話。

結束通話

如要結束通話,請按照下列步驟操作:

  1. 在使用者終止呼叫時呼叫 setDisconnected(DisconnectCause) 做為參數傳送 LOCAL,如果另一方終止呼叫,則傳送 REMOTE 做為參數。
  2. 呼叫 destroy() 方法。

呼叫限制

為了向使用者提供一致且簡單的通話體驗,電信架構對於管理裝置上的呼叫,會強制執行一些限制。舉例來說,假設使用者安裝了兩個呼叫應用程式,而這些應用程式實作了自行管理的 ConnectionService API、FooTalk 和 Bard。這種情況適用下列限制:

  • 在搭載 API 級別 27 或以下級別的裝置上,只有一個應用程式能夠隨時維持進行中的呼叫。這項限製表示使用者透過 FooTalk 應用程式進行通話時,BarTalk 應用程式無法發起或接收新的呼叫。

    在搭載 API 級別 28 以上版本的裝置上,如果 FooTalk 和 BarTalk 同時宣告 CAPABILITY_SUPPORT_HOLDCAPABILITY_HOLD 權限,使用者就能在應用程式間切換以啟動或接聽其他呼叫,藉此維持多個進行中的呼叫。

  • 如果使用者進行一般受管理的呼叫 (例如使用內建電話或撥號應用程式),使用者就無法參與透過通話應用程式發起的通話。也就是說,如果使用者是透過行動電信業者進行一般通話,他們也無法同時參與 FooTalk 或 BarTalk 通話。

  • 如果使用者撥打緊急電話,電信子系統會中斷應用程式通話。

  • 使用者正在撥打緊急電話時,應用程式將無法接收或撥打電話。

  • 如果應用程式收到來電時,在其他呼叫應用程式中有進行中的通話,接聽來電會結束其他應用程式中的任何進行中的通話。您不應顯示平常的來電使用者介面。電信架構會顯示來電使用者介面,然後通知使用者接聽新呼叫時會結束目前的通話。也就是說,如果使用者正在使用 FooTalk 通話,而 BarTalk 應用程式收到來電,電信架構會通知使用者他們有新的傳入 BarTalk 呼叫,且一旦接聽 BarTalk 呼叫,就會結束他們的 FooTalk 通話。

成為預設電話應用程式

預設的撥號/電話應用程式會在裝置通話時提供通話使用者介面。使用者也能透過這個方式撥打電話,以及查看裝置上的通話記錄。裝置隨附系統提供的預設撥號程式/手機應用程式。使用者可以選擇單一應用程式,從系統應用程式接管這個角色。想要完成這個角色的應用程式會使用 RoleManager 要求填入 RoleManager.ROLE_DIALER 角色。

預設的手機應用程式是在裝置通話期間提供使用者介面,但裝置未處於車用模式 (即 UiModeManager#getCurrentModeType() 不是 Configuration.UI_MODE_TYPE_CAR)。

如要填入 RoleManager.ROLE_DIALER 角色,應用程式必須符合下列幾項條件:

  • 而是必須處理 Intent#ACTION_DIAL 意圖。也就是說,應用程式必須提供撥號鍵盤 UI,讓使用者撥打電話。
  • 應用程式必須完全實作 InCallService API,並同時提供傳入呼叫 UI 和進行中呼叫 UI。

注意:如果填入 RoleManager.ROLE_DIALER 的應用程式在繫結期間傳回 null InCallService,電信架構會自動改回使用裝置上預先載入的撥號應用程式。系統會向使用者顯示通知,告知使用者已繼續使用預先載入的撥號應用程式進行通話。您的應用程式不應傳回 null 繫結;如此一來,就不符合 RoleManager.ROLE_DIALER 的要求。

注意:如果應用程式會填入 RoleManager.ROLE_DIALER,並在執行階段進行變更,導致應用程式不再符合這個角色的要求,RoleManager 會自動從應用程式中移除應用程式,並關閉應用程式。例如,如果您使用 PackageManager.setComponentEnabledSetting(ComponentName, int, int) 以程式輔助方式停用應用程式資訊清單中宣告的 InCallService,應用程式將不再符合 RoleManager.ROLE_DIALER 預期的要求。

當使用者撥打緊急電話時,即使應用程式已填入 RoleManager.ROLE_DIALER 角色,預先載入的撥號程式「一律」會使用。為確保在撥打緊急電話時提供最佳體驗,預設撥號程式應一律使用 TelecomManager.placeCall(Uri, Bundle) 撥打電話 (包括緊急電話)。這可確保平台能驗證要求是否來自預設撥號程式。如果未預先載入的撥號應用程式使用 Intent#ACTION_CALL 撥打緊急電話,系統會將該應用程式透過 Intent#ACTION_DIAL 傳送至預先載入的撥號應用程式進行確認。這並非最佳使用者體驗。

以下是 InCallService 的資訊清單註冊範例。中繼資料 TelecomManager#METADATA_IN_CALL_SERVICE_UI 表示這個特定的 InCallService 實作旨在取代內建呼叫 UI。中繼資料 TelecomManager#METADATA_IN_CALL_SERVICE_RINGING 表示此 InCallService 將播放來電的鈴聲。如要進一步瞭解如何顯示來電 UI 及在應用程式中播放鈴聲,請參閱下文

 <service android:name="your.package.YourInCallServiceImplementation"
          android:permission="android.permission.BIND_INCALL_SERVICE"
          android:exported="true">
      <meta-data android:name="android.telecom.IN_CALL_SERVICE_UI" android:value="true" />
      <meta-data android:name="android.telecom.IN_CALL_SERVICE_RINGING"
          android:value="true" />
      <intent-filter>
          <action android:name="android.telecom.InCallService"/>
      </intent-filter>
 </service>

注意:您不應使用 android:exported="false" 屬性標記 InCallService,否則可能會導致在呼叫期間無法繫結至實作項目。

除了實作 InCallService API 之外,您也必須在資訊清單中宣告處理 Intent#ACTION_DIAL 意圖的活動。以下範例說明如何完成這項操作:

 <activity android:name="your.package.YourDialerActivity"
           android:label="@string/yourDialerActivityLabel">
      <intent-filter>
           <action android:name="android.intent.action.DIAL" />
           <category android:name="android.intent.category.DEFAULT" />
      </intent-filter>
      <intent-filter>
           <action android:name="android.intent.action.DIAL" />
           <category android:name="android.intent.category.DEFAULT" />
           <data android:scheme="tel" />
      </intent-filter>
 </activity>

當使用者安裝並執行您的應用程式時,您應該使用 RoleManager 提示使用者,瞭解他們是否要將應用程式設為新的預設手機應用程式。

以下程式碼顯示應用程式如何要求成為預設的手機/撥號應用程式:

 private static final int REQUEST_ID = 1;

 public void requestRole() {
     RoleManager roleManager = (RoleManager) getSystemService(ROLE_SERVICE);
     Intent intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER);
     startActivityForResult(intent, REQUEST_ID);
 }

 public void onActivityResult(int requestCode, int resultCode, Intent data) {
     if (requestCode == REQUEST_ID) {
         if (resultCode == android.app.Activity.RESULT_OK) {
             // Your app is now the default dialer app
         } else {
             // Your app is not the default dialer app
         }
     }
 }

存取穿戴式裝置的 InCallService

    如果您的應用程式是第三方隨附應用程式,且想要存取 InCallService API,則該應用程式可執行下列操作:

    1. 在資訊清單中宣告 MANAGE_ONGOING_CALLS 權限
    2. 以隨附應用程式的形式,透過 CompanionDeviceManager API 與實體穿戴式裝置建立關聯。請參閱:https://developer.android.com/guide/topics/connectivity/companion-device-pairing
    3. 使用 BIND_INCALL_SERVICE 權限實作這個 InCallService

顯示來電通知

應用程式透過 InCallService#onCallAdded(Call) 收到新的來電時,會顯示來電的呼叫 UI。方法是使用 NotificationManager API 發布新的來電通知。

如果應用程式宣告中繼資料 TelecomManager#METADATA_IN_CALL_SERVICE_RINGING,就會負責播放來電的鈴聲。應用程式應建立指定所需鈴聲的 NotificationChannel。例如:

 NotificationChannel channel = new NotificationChannel(YOUR_CHANNEL_ID, "Incoming Calls",
          NotificationManager.IMPORTANCE_MAX);
 // other channel setup stuff goes here.

 // We'll use the default system ringtone for our incoming call notification channel.  You can
 // use your own audio resource here.
 Uri ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
 channel.setSound(ringtoneUri, new AudioAttributes.Builder()
          // Setting the AudioAttributes is important as it identifies the purpose of your
          // notification sound.
          .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
          .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
      .build());

 NotificationManager mgr = getSystemService(NotificationManager.class);
 mgr.createNotificationChannel(channel);

應用程式收到新來電時,會為來電建立 Notification,並將其與來電通知管道建立關聯。您可在通知中指定 PendingIntent,以便啟動全螢幕來電 UI。如果使用者積極使用手機,通知管理員架構會將通知顯示為抬頭通知。當使用者沒有使用手機時,系統會改用全螢幕來電 UI。例如:

 // Create an intent which triggers your fullscreen incoming call user interface.
 Intent intent = new Intent(Intent.ACTION_MAIN, null);
 intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK);
 intent.setClass(context, YourIncomingCallActivity.class);
 PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
 // Build the notification as an ongoing high priority item; this ensures it will show as
 // a heads up notification which slides down over top of the current content.
 final Notification.Builder builder = new Notification.Builder(context);
 builder.setOngoing(true);
 builder.setPriority(Notification.PRIORITY_HIGH);
 // Set notification content intent to take user to the fullscreen UI if user taps on the
 // notification body.
 builder.setContentIntent(pendingIntent);
 // Set full screen intent to trigger display of the fullscreen UI when the notification
 // manager deems it appropriate.
 builder.setFullScreenIntent(pendingIntent, true);
 // Setup notification content.
 builder.setSmallIcon( yourIconResourceId );
 builder.setContentTitle("Your notification title");
 builder.setContentText("Your notification content.");
 // Use builder.addAction(..) to add buttons to answer or reject the call.
 NotificationManager notificationManager = mContext.getSystemService(
     NotificationManager.class);
 notificationManager.notify(YOUR_CHANNEL_ID, YOUR_TAG, YOUR_ID, builder.build());
```