預設手機應用程式可讓 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
類別實作例項 (詳情請參閱「實作連線」),代表新的撥出呼叫。您可以執行下列動作,進一步自訂傳出連線:- 應用程式應使用
PROPERTY_SELF_MANAGED
常數做為引數來呼叫setConnectionProperties(int)
方法,指出連線源自呼叫應用程式。 - 如果您的應用程式支援保留呼叫,請呼叫
setConnectionCapabilities(int)
方法,並將引數設為CAPABILITY_HOLD
和CAPABILITY_SUPPORT_HOLD
常數的位元遮罩值。 - 如要設定呼叫端名稱,請使用
setCallerDisplayName(String, int)
方法傳遞PRESENTATION_ALLOWED
常數做為int
參數,表示應顯示呼叫端的名稱。 - 為確保撥出的呼叫具有適當的視訊狀態,請呼叫
Connection
物件的setVideoState(int)
方法,並傳送ConnectionRequest
物件的getVideoState()
方法傳回的值。
- 應用程式應使用
onCreateOutgoingConnectionFailed(PhoneAccountHandle, ConnectionRequest)
當應用程式呼叫
placeCall(Uri, Bundle)
方法,導致撥出電話時,電信子系統會呼叫這個方法。為因應這種情況,應用程式應通知使用者 (例如使用快訊方塊或浮動式訊息) 無法撥出電話。如果緊急電話正在進行,或是另一個應用程式正在進行通話,但無法在撥打電話前將通話保留,則應用程式可能無法撥打電話。onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest)
當應用程式呼叫
addNewIncomingCall(PhoneAccountHandle, Bundle)
方法,向系統通知應用程式中有新的來電時,電信子系統會呼叫這個方法。應用程式會傳回Connection
實作的新例項 (詳情請參閱「實作連線」以代表新的來電)。您可以執行下列動作,進一步自訂連入連線:- 應用程式應使用
PROPERTY_SELF_MANAGED
常數做為引數來呼叫setConnectionProperties(int)
方法,指出連線源自呼叫應用程式。 - 如果您的應用程式支援保留呼叫,請呼叫
setConnectionCapabilities(int)
方法,並將引數設為CAPABILITY_HOLD
和CAPABILITY_SUPPORT_HOLD
常數的位元遮罩值。 - 如要設定呼叫端名稱,請使用
setCallerDisplayName(String, int)
方法傳遞PRESENTATION_ALLOWED
常數做為int
參數,表示應顯示呼叫端的名稱。 - 如要指定來電的電話號碼或地址,請使用
Connection
物件的setAddress(Uri, int)
方法。 - 為確保撥出的呼叫具有適當的視訊狀態,請呼叫
Connection
物件的setVideoState(int)
方法,並傳送ConnectionRequest
物件的getVideoState()
方法傳回的值。
- 應用程式應使用
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 處理這些情境。
接聽來電
無論其他應用程式是否有呼叫,處理來電的流程都會改變。各流程的不同原因在於,電信架構必須在其他應用程式有有效呼叫時建立一些限制,以確保裝置上所有呼叫的應用程式都有穩定的環境。詳情請參閱「呼叫限制」。
其他應用程式中沒有進行中的通話
如要在其他應用程式沒有進行中的通話時接聽來電,請按照下列步驟操作:
- 您的應用程式會透過一般機制接收新的來電。
- 使用
addNewIncomingCall(PhoneAccountHandle, Bundle)
方法將新來電通知電信子系統。 - 電信子系統會繫結至應用程式的
ConnectionService
實作,並使用onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest)
方法要求Connection
類別的新執行個體,代表新的傳入呼叫。 - 電信子系統通知應用程式應使用
onShowIncomingCallUi()
方法顯示來電使用者介面。 - 應用程式會使用含有關聯全螢幕意圖的通知,顯示傳入的 UI。詳情請參閱
onShowIncomingCallUi()
。 - 如果使用者接受來電,請呼叫
setActive()
方法;或setDisconnected(DisconnectCause)
指定REJECTED
做為參數,然後在使用者拒絕來電時呼叫destroy()
方法。
其他應用程式正在進行無法保留的通話
如要在其他應用程式進行無法保留的通話時接聽來電,請按照下列步驟操作:
- 您的應用程式會透過一般機制接收新的來電。
- 使用
addNewIncomingCall(PhoneAccountHandle, Bundle)
方法將新來電通知電信子系統。 - 電信子系統會繫結至應用程式的
ConnectionService
實作,並使用onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest)
方法要求新的Connection
物件例項,代表新來電。 - 電信子系統會顯示來電的來電 UI。
- 如果使用者接聽來電,電信子系統會呼叫
onAnswer()
方法。應呼叫setActive()
方法,向電話子系統指出呼叫已連線。 - 如果使用者拒絕通話,電信子系統會呼叫
onReject()
方法。您應呼叫setDisconnected(DisconnectCause)
方法,將REJECTED
指定為參數,然後呼叫destroy()
方法。
撥出電話
撥打撥出呼叫的流程會處理因電信架構設下的限制,而無法呼叫呼叫的可能性。詳情請參閱「呼叫限制」。
如要撥打電話,請按照下列步驟操作:
- 使用者在應用程式中撥出電話。
- 使用
placeCall(Uri, Bundle)
方法,通知電信子系統新的撥出電話。請留意方法參數的注意事項:Uri
參數代表呼叫的所在地址。如果是一般電話號碼,請使用tel:
URI 配置。Bundle
參數可讓您將應用程式的PhoneAccountHandle
物件新增至EXTRA_PHONE_ACCOUNT_HANDLE
額外項目,以提供呼叫應用程式的相關資訊。應用程式必須為每個撥出呼叫提供PhoneAccountHandle
物件。- 透過
Bundle
參數,您也可以在EXTRA_START_CALL_WITH_VIDEO_STATE
額外項目中指定STATE_BIDIRECTIONAL
值,藉此指明撥出電話是否包含影片。在此情況下,電信子系統預設會將視訊通話傳送至喇叭。
- 電信子系統會繫結至應用程式的
ConnectionService
實作。 - 如果應用程式無法撥出電話,電信子系統會呼叫
onCreateOutgoingConnectionFailed(PhoneAccountHandle, ConnectionRequest)
方法,通知應用程式目前無法撥打電話。應用程式應告知使用者無法撥打電話。 - 如果應用程式能夠撥打電話,電信子系統會呼叫
onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)
方法。應用程式應傳回Connection
類別的執行個體,代表新的撥出呼叫。如要進一步瞭解應在連線中設定的屬性,請參閱「實作連線服務」。 - 撥出電話後,請呼叫
setActive()
方法,通知電信子系統正在進行通話。
結束通話
如要結束通話,請按照下列步驟操作:
- 在使用者終止呼叫時呼叫
setDisconnected(DisconnectCause)
做為參數傳送LOCAL
,如果另一方終止呼叫,則傳送REMOTE
做為參數。 - 呼叫
destroy()
方法。
呼叫限制
為了向使用者提供一致且簡單的通話體驗,電信架構對於管理裝置上的呼叫,會強制執行一些限制。舉例來說,假設使用者安裝了兩個呼叫應用程式,而這些應用程式實作了自行管理的 ConnectionService
API、FooTalk 和 Bard。這種情況適用下列限制:
在搭載 API 級別 27 或以下級別的裝置上,只有一個應用程式能夠隨時維持進行中的呼叫。這項限製表示使用者透過 FooTalk 應用程式進行通話時,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
意圖。也就是說,應用程式必須提供撥號鍵盤 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,則該應用程式可執行下列操作:
- 在資訊清單中宣告 MANAGE_ONGOING_CALLS 權限
- 以隨附應用程式的形式,透過
CompanionDeviceManager
API 與實體穿戴式裝置建立關聯。請參閱:https://developer.android.com/guide/topics/connectivity/companion-device-pairing - 使用 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());
```