构建默认手机应用

借助默认电话应用,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>

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

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

 <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

<ph type="x-smartling-placeholder">
    </ph> 如果您的应用是第三方配套应用,并且想要访问 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());
```