建構預設手機應用程式

預設手機應用程式可讓 Android Telecom 架構使用角色管理員和通話服務來取代 Android 裝置上的預設手機應用程式,並實作 InCallService API,藉此通知應用程式通話狀態。導入作業必須符合下列規定:

不得具有任何呼叫功能,且只能以呼叫使用者介面而建立。 它必須處理 Telecom 架構知道的所有呼叫,不對呼叫性質做出假設。舉例來說,不得假定通話是以 SIM 卡為基礎的通話,或導入根據任何 ConnectionService 做出的通話限制,例如強制執行視訊通話的電話通訊限制。

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

呼叫應用程式範例
範例:使用自己的使用者介面發出呼叫的應用程式

Android 架構包含 android.telecom 套件, 包含類別,可協助您根據電信服務建構呼叫應用程式 這個架構的重點在於根據電信架構建構應用程式, 以下優點:

  • 您的應用程式與 裝置。
  • 您的應用程式與其他同樣遵守 這個原則
  • 架構有助於應用程式管理音訊和影片轉送作業。
  • 架構有助應用程式判斷呼叫是否為焦點。

資訊清單宣告和權限

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

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

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

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

<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 實作項目,並要求新的例項 代表新傳入的 Connection 類別 呼叫 onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest) 方法。
  4. 電信子系統會通知應用程式,應顯示來電 透過 onShowIncomingCallUi() 方法建立使用者介面
  5. 您的應用程式使用與 全螢幕意圖詳情請參閱 onShowIncomingCallUi()
  6. 如果使用者已看過 setActive() 方法,請呼叫該方法。 接受來電,或 setDisconnected(DisconnectCause)REJECTED 指定為參數,後接 呼叫 destroy() 方法 拒接來電。

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

在其他應用程式有進行中的通話時,在無法接聽來電的情況下接聽來電 進入保留狀態,請按照下列步驟操作:

  1. 應用程式會透過一般機制接收新的來電。
  2. 使用 addNewIncomingCall(PhoneAccountHandle, Bundle) 方法 通知電信子系統有新來電。
  3. 電信子系統繫結至應用程式的 ConnectionService 實作項目,並要求新的例項 Connection 物件,代表新的 呼叫 onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest) 方法。
  4. 電信子系統會顯示來電的使用者介面。
  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. 如果使用者有使用者,請呼叫傳送 LOCAL 做為參數的 setDisconnected(DisconnectCause) 終止通話,或傳送 REMOTE 給你 。
  2. 呼叫 destroy() 方法。

呼叫限制

為了確保使用者享有一致且簡易的通話體驗,「電信」應用程式 架構會強制執行一些管理裝置上的呼叫限制。適用對象 舉例來說,假設使用者安裝了兩個 自行管理的 ConnectionService API、FooTalk 和 BarTalk。在此情況下,系統會套用以下限制條件:

  • 在搭載 API 級別 27 以下版本的裝置上,只有一個應用程式能維護 接聽程式。這項限制意味著 進行中的通話時,BarTalk 應用程式無法啟動或接聽 重新撥號。

    在搭載 API 級別 28 以上版本的裝置上 (如果同時有 FooTalk 和 BarTalk 宣告 CAPABILITY_SUPPORT_HOLD敬上 和 CAPABILITY_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 意圖。換句話說,應用程式必須提供 撥號鍵盤使用者介面,供使用者撥打電話。
  • 必須完全實作 InCallService API,並提供來電 使用者介面,以及持續呼叫的 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 會在收到來電時播放鈴聲。詳情請見 請參閱下文,進一步瞭解如何顯示來電 應用程式中的使用者介面和鈴聲。

 <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>

注意:請勿以屬性標示 InCallService android:exported="false";以免無法繫結至實作項目 。

除了實作 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。 例如:

 // 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());
```