构建默认手机应用

默认电话应用可让 Android Telecom 框架使用角色管理器和通话服务为 Android 设备上的默认电话应用创建替换项,从而将通话状态告知您的应用,从而实现 InCallService API。您的实现必须满足以下要求:

且不得包含任何通话功能,且必须仅包含用于通话的界面。 它必须处理 Telecom 框架知道的所有通话,而不能对通话的性质做出假设。例如,应用不得假定通话是基于 SIM 卡的电话通话,也不得实现基于任何一项 ConnectionService 的通话限制(例如对视频通话强制执行电话限制)。

借助通话应用,用户可以在自己的设备上接听或拨打语音或视频通话。通话应用使用自己的界面(而不是默认的“电话”应用界面)显示通话,如以下屏幕截图所示。

通话应用示例
使用自己界面的通话应用示例

Android 框架包含 android.telecom 软件包,其中包含可帮助您根据 Telecom 框架构建通话应用的类。根据 Telecom 框架构建应用具有以下优势:

  • 您的应用可以与设备中的原生 Telecom 子系统正常进行互操作。
  • 您的应用可以与其他同样遵循该框架的通话应用正常进行互操作。
  • 该框架有助于您的应用管理音频和视频转接。
  • 该框架有助于您的应用确定其通话是否具有焦点。

清单声明和权限

在应用清单中,声明您的应用使用 MANAGE_OWN_CALLS 权限,如以下示例所示:

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

如需详细了解如何声明应用权限,请参阅权限

您必须声明一项服务,该服务指定用于在您的应用中实现 ConnectionService 类的类。Telecom 子系统要求该服务声明 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>

如需详细了解如何声明应用组件(包括服务),请参阅应用组件

实现连接服务

您的通话应用必须提供 Telecom 子系统可以绑定到的 ConnectionService 类的实现。 您的 ConnectionService 实现应替换以下方法:

onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)

Telecom 子系统调用此方法以响应您的应用对 placeCall(Uri, Bundle) 的调用,从而创建新的去电。您的应用会返回 Connection 类实现的新实例(如需了解详情,请参阅实现连接)以表示新的去电。您可以通过执行以下操作进一步自定义去电连接:

onCreateOutgoingConnectionFailed(PhoneAccountHandle, ConnectionRequest)

Telecom 子系统会在您的应用调用 placeCall(Uri, Bundle) 方法且无法拨出电话时调用此方法。为了应对这种情况,您的应用应告知用户(例如,使用提醒框或消息框)无法拨出电话。如果有正在进行的紧急呼叫或者在您拨打电话之前另一个应用中正在进行的通话无法置于保持状态,您的应用可能无法拨打电话。

onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest)

Telecom 子系统会在您的应用调用 addNewIncomingCall(PhoneAccountHandle, Bundle) 方法时调用此方法,以告知系统您的应用中有新的来电。您的应用会返回 Connection 实现的新实例(如需了解详情,请参阅实现连接)以表示新的来电。您可以通过执行以下操作进一步自定义来电连接:

onCreateIncomingConnectionFailed(PhoneAccountHandle, ConnectionRequest)

Telecom 子系统会在您的应用调用 addNewIncomingCall(PhoneAccountHandle, Bundle) 方法时调用此方法,以告知 Telecom 有新的来电,但此来电不受允许(如需了解详情,请参阅通话限制)。您的应用应拒绝来电且不发出提示音,可以选择发布通知以告知用户有未接电话。

实现连接

您的应用应创建 Connection 的子类以表示应用中的来电。您应在实现中替换以下方法:

onShowIncomingCallUi()

Telecom 子系统会在您添加新的来电时调用此方法,并且您的应用应显示其来电界面。

onCallAudioStateChanged(CallAudioState)

Telecom 子系统调用此方法来告知您的应用当前音频路径或模式已发生更改。这样做是为了响应您的应用使用 setAudioRoute(int) 方法对音频模式做出的更改。如果系统更改了音频路径(例如,蓝牙耳机断开连接时),也可以调用此方法。

onHold()

当 Telecom 子系统想要将通话置于保持状态时,就会调用此方法。 为了响应此请求,您的应用应保持通话,然后调用 setOnHold() 方法以告知系统通话正置于保持状态。Telecom 子系统可能会在显示通话的通话服务(例如 Android Auto)想要中继用户将通话置于保持状态的请求时调用此方法。如果用户在另一个应用中进行通话,Telecom 子系统也会调用此方法。如需详细了解通话服务,请参阅 InCallService

onUnhold()

当 Telecom 子系统想要恢复已置于保持状态的通话时,就会调用此方法。您的应用恢复通话后,应调用 setActive() 方法以告知系统该通话已不再置于保持状态。当显示通话的通话服务(例如 Android Auto)想要中继恢复通话的请求时,Telecom 子系统可能会调用此方法。如需详细了解通话服务,请参阅 InCallService

onAnswer()

Telecom 子系统调用此方法以告知您的应用应该接听来电。当应用接听来电后,应调用 setActive() 方法以告知系统已接听来电。如果您的应用添加了新的来电,并且另一个应用中已经有一个无法置于保持状态的正在进行的通话,Telecom 子系统可能会调用此方法。在这些情况下,Telecom 子系统会代表您的应用显示来电界面。该框架提供了一种过载方法,支持指定接听来电的视频状态。如需了解详情,请参阅 onAnswer(int)

onReject()

Telecom 子系统会在想要拒接来电时调用此方法。您的应用拒接来电后,应调用 setDisconnected(DisconnectCause) 并指定 REJECTED 作为参数。然后,您的应用应调用 destroy() 方法以告知系统应用已对来电进行处理。当用户拒接了来自您应用的来电时,Telecom 子系统会调用此方法。

onDisconnect()

Telecom 子系统会在它要断开通话连接时调用此方法。 通话结束后,您的应用应调用 setDisconnected(DisconnectCause) 方法并将 LOCAL 指定为参数,以表明用户请求导致通话中断。然后,您的应用应调用 destroy() 方法以告知 Telecom 子系统应用已对来电进行处理。当用户通过其他通话服务(例如 Android Auto)断开通话连接时,系统可能会调用此方法。当您必须断开通话连接以允许拨打其他电话时(例如,如果用户想要进行紧急呼叫),系统也会调用此方法。如需详细了解通话服务,请参阅 InCallService

处理常见的通话场景

在调用流程中使用 ConnectionService API 涉及与 android.telecom 软件包中的其他类进行互动。以下部分介绍了常见的通话场景以及应用应如何使用 API 来处理这些场景。

接听来电

无论其他应用中是否存在来电,处理来电的流程都会发生变化。流程之所以会出现差异,是因为当其他应用中有正在进行的通话时,Telecom 框架必须建立一些限制条件,以确保设备上的所有通话应用都能有一个稳定的环境。如需了解详情,请参阅通话限制

其他应用中没有正在进行的通话

如需在其他应用中没有正在进行的通话时接听来电,请按以下步骤操作:

  1. 您的应用会根据常规机制接收新来电。
  2. 使用 addNewIncomingCall(PhoneAccountHandle, Bundle) 方法在有新来电时告知 Telecom 子系统。
  3. Telecom 子系统绑定到您应用的 ConnectionService 实现,并使用 onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest) 方法请求表示新来电的 Connection 类的新实例。
  4. Telecom 子系统会使用 onShowIncomingCallUi() 方法告知您的应用应显示其来电界面。
  5. 您的应用会通过具有相关联的全屏 Intent 的通知来显示其来电界面。如需了解详情,请参阅 onShowIncomingCallUi()
  6. 如果用户接听来电,则调用 setActive() 方法;如果用户拒接来电,则调用 setDisconnected(DisconnectCause),以将 REJECTED 指定为参数,然后调用 destroy() 方法。

其他应用中无法置于保持状态的正在进行的通话

如需在其他应用中有无法置于保持状态的正在进行的通话时接听来电,请按以下步骤操作:

  1. 您的应用会根据常规机制接收新来电。
  2. 使用 addNewIncomingCall(PhoneAccountHandle, Bundle) 方法在有新来电时告知 Telecom 子系统。
  3. Telecom 子系统绑定到您应用的 ConnectionService 实现,并使用 onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest) 方法请求表示新来电的 Connection 对象的新实例。
  4. Telecom 子系统会为您的来电显示来电界面。
  5. 如果用户接听该来电,Telecom 子系统会调用 onAnswer() 方法。您应调用 setActive() 方法,以告知 Telecom 子系统来电现已连接。
  6. 如果用户拒接该来电,Telecom 子系统会调用 onReject() 方法。您应调用 setDisconnected(DisconnectCause) 方法,以将 REJECTED 指定为参数,然后调用 destroy() 方法。

拨出电话

拨出电话流程涉及处理以下情况:由于 Telecom 框架施加的限制而导致可能无法拨打电话。如需了解详情,请参阅通话限制

如需拨出电话,请按以下步骤操作:

  1. 用户在您的应用内发起去电。
  2. 使用 placeCall(Uri, Bundle) 方法告知 Telecom 子系统要新拨出电话。考虑以下关于方法参数的注意事项:
    • Uri 参数表示拨出电话的地址。对于普通电话号码,请使用 tel: URI 架构。
    • 通过 Bundle 参数,您可以将应用的 PhoneAccountHandle 对象添加到 EXTRA_PHONE_ACCOUNT_HANDLE extra,以提供有关通话应用的信息。您的应用必须为每个去电提供 PhoneAccountHandle 对象。
    • 通过 Bundle 参数,您还可以在 EXTRA_START_CALL_WITH_VIDEO_STATE extra 中指定 STATE_BIDIRECTIONAL 值,从而指定去电是否包含视频。请注意,默认情况下,Telecom 子系统会将视频通话转接到扬声器。
  3. Telecom 子系统绑定到您应用的 ConnectionService 实现。
  4. 如果您的应用无法拨出电话,Telecom 子系统会调用 onCreateOutgoingConnectionFailed(PhoneAccountHandle, ConnectionRequest) 方法以告知您的应用目前无法拨打电话。您的应用应告知用户无法拨打电话。
  5. 如果您的应用能够拨出电话,Telecom 子系统会调用 onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest) 方法。您的应用应返回 Connection 类的实例以表示新去电。如需详细了解应在连接中设置的属性,请参阅实现连接服务
  6. 去电连接后,调用 setActive() 方法,以告知 Telecom 子系统通话正在进行中。

结束通话

如需结束通话,请按以下步骤操作:

  1. 如果用户终止了通话,则调用 setDisconnected(DisconnectCause) 并发送 LOCAL 作为参数;如果对方终止了通话,则发送 REMOTE 作为参数。
  2. 调用 destroy() 方法。

通话限制

为了确保为用户提供一致且简单的通话体验,Telecom 框架针对管理设备上的通话施加了一些限制。例如,假设用户安装了两个通话应用,并且这两个应用实现了自行管理的 ConnectionService API:FooTalk 和 BarTalk。在这种情况下,应遵循以下限制条件:

  • 在搭载 API 级别 27 或更低版本的设备上,任何时候都只有一个应用可以保持当前通话。这种限制意味着,当用户使用 FooTalk 应用进行通话时,BarTalk 应用无法发起或接听新的通话。

    在搭载 API 级别 28 或更高版本的设备上,如果 FooTalk 和 BarTalk 都声明了 CAPABILITY_SUPPORT_HOLDCAPABILITY_HOLD 权限,用户就可以通过在两个应用之间进行切换发起或接收另一个通话,从而保持多个正在进行的通话。

  • 如果用户正在进行常规的受管理通话(例如,使用内置的电话或拨号器应用),用户无法进行来自通话应用的通话。也就是说,如果用户使用移动运营商正常通话,则无法同时进行 FooTalk 或 BarTalk 通话。

  • 如果用户拨打紧急呼叫,Telecom 子系统会断开应用的通话连接。

  • 当用户进行紧急呼叫时,您的应用无法接听或拨出电话。

  • 如果在应用收到来电时,另一个通话应用中有正在进行的通话,接听来电会结束另一个应用中的当前通话。您的应用不应显示其正常的来电界面。Telecom 框架会显示来电界面,并告知用户接听新来电会结束当前通话。也就是说,如果用户正在进行 FooTalk 通话,而 BarTalk 应用收到来电,Telecom 框架会告知用户有新的 BarTalk 来电,并且接听 BarTalk 通话会结束 FooTalk 通话。

成为默认电话应用

默认拨号器/电话应用是在设备通话时提供通话界面的应用。它还为用户提供了一种在其设备上拨打电话和查看通话记录的方法。设备捆绑了系统提供的默认拨号器/电话应用。用户可以从系统应用中选择单个应用来接管此角色。希望实现此角色的应用会使用 RoleManager 请求填充 RoleManager.ROLE_DIALER 角色。

默认的电话应用会在设备处于通话状态且设备未处于车载模式时提供界面(即,UiModeManager#getCurrentModeType() 不是 Configuration.UI_MODE_TYPE_CAR)。

要填充 RoleManager.ROLE_DIALER 角色,应用必须满足以下要求:

  • 它必须处理 Intent#ACTION_DIAL intent。这意味着应用必须提供拨号键盘界面,以便用户拨出电话。
  • 它必须完全实现 InCallService API,并同时提供来电界面和正在进行的通话界面。

注意:如果填充 RoleManager.ROLE_DIALER 的应用在绑定期间返回 null InCallService,则 Telecom 框架将自动回退为使用设备上预加载的拨号器应用。系统将向用户显示通知,让用户知道通话已在使用预加载的拨号器应用进行。您的应用绝不应返回 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 实现旨在替换内置的通话界面。 元数据 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>

注意:您不应使用 android:exported="false" 属性标记 InCallService;否则可能会导致在调用期间无法绑定到您的实现。

除了实现 InCallService API 之外,您还必须在清单中声明用于处理 Intent#ACTION_DIAL intent 的 Activity。以下示例说明了具体方法:

 <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) 收到新的来电时,它负责为来电显示来电界面。为此,它应使用 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,以启动全屏来电界面。如果用户正在使用手机,通知管理器框架会以浮动通知的形式显示您的通知。当用户未使用手机时,系统会使用您的全屏来电界面。 例如:

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