【融云技术】如何使用 Telecom 构建 VOIP 通话应用,提高APP用户体验?

【融云技术】如何使用 Telecom 构建 VOIP 通话应用,提高APP用户体验?

文 / 周加涛

Android 收紧权限后,如何利用 Telecom 技术构建 VOIP 通话应用,提升 App 用户的使用体验相信是开发者比较关心的技术话题之一。本文从 Telecom 框架概览、Telecom 启动过程分析、构建 VOIP 通话应用、处理常见的通话场景几个方面,带领大家领略融云如何使用 Telecom 构建 VOIP 通话应用。

一 、背景及现状

Android 低版本收到 IM 语音通话,通过在后台启动 Activity,可以直接跳转至通话界面;但 Android 10 及更高版本上收到 IM 语音通话时,只能通过弹出通知栏的方式提醒用户,这种方式不直观,也可能漏接通话,用户体验不佳。

因为从 Android 10 开始,系统限制了从后台(组件)启动 Activity 的行为。这样有助于最大限度地减少对用户造成的中断,让用户更好地控制其屏幕上显示的内容,增加用户体验和安全性。后台应用启动,必须通过“显示通知”的方式来进行,通知用户借助 notification 来启动新的 Activity。

在本文中重点介绍 Android Telecom 及使用 Telecom 构建 VOIP 通话应用,在 Android 高版本实现 IM 语音通话的来电界面跳转。

二、Telecom 框架概览

Android Telecom 框架管理 Android 设备上的音频和视频呼叫。其中包括基于 SIM 的呼叫(例如,使用Telephony 框架)和由 ConnectionService API 实施者提供的 VOIP 呼叫。

如图一所示,Telecom 负责通话状态消息上报和通话控制消息下发。

图一:Telecom 消息处理模型

通话消息下发流程:用户在通话界面操作后,通过 InCallAdapter(IInCallAdapter服务端实现)对象来通知Telecom(最终调用CallsManager相关函数),Telecom 调用 IConnectionService 封装接口向 RIL 发起通话管理和控制相关RIL请求,RIL 转换成对应的消息发送给 Modem 执行,其中包括拨号,接听电话,拒接电话等。

通话状态更新消息上报流程:RIL 收到 Modem 的通话状态变化, 通知当前 Connection 和 Call 的状态或属性发生的变化,再经过一系列的Listener 消息回调处理,最终由 lnCallController 创建 ParcelableCall 对象,使用 llnCallService 服务接口调用发送给 Dialer 应用。 Telecom 起交互桥梁作用,与 InCallUI 和 Telephony【Phone进程、TeleService】交互,交互过程如图二所示:

图二:Telecom 模块间交互流程

除 phone 进程和 modem 通过 socket 进行通信外,进程间通过aidl进行交互,Telecom 进程分别通过 IConnectionService.aid l和 IInCallService.aidl 来控制 phone 进程和Incallui 进程。而phone进程(com.android.phone)和Incallui进程(com.android.incallui)通过 IConnectionServiceAdapter.aidl 和 IInCallAdapter.aidl 来通知 telecom 进程需要变更。

三、Telecom 启动过程分析

· /frameworks/base/services/java/com/android/server/SystemServer.javaSystemServer 进程初始化完成后, startOtherServices 中启动 TelecomLoaderService 系统服务,加载 Telecom:

/**
* Starts a miscellaneous grab bag of stuff that has yet to be refactored and organized.
*/
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
...
//启动TelecomLoaderService系统服务,用于加载Telecom t.traceBegin("StartTelecomLoaderService");
mSystemServiceManager.startService(TelecomLoaderService.class);
t.traceEnd();
...
}

→· /frameworks/base/services/core/java/com/android/server/telecom/TelecomLoaderService.java
TelecomLoaderService 类中 onBootPhase 函数,用于 SystemServer 告知系统服务目前系统启动所处的阶段。AMS(ActivityManagerService) 启动完成后,开始连接 Telecom服务。

/**
* Starts the telecom component by binding to its ITelecomService implementation. Telecom is setup
* to run in the system-server process so once it is loaded into memory it will stay running.
* @hide
*/
public class TelecomLoaderService extends SystemService {
private static final ComponentName SERVICE_COMPONENT = new ComponentName(
"com.android.server.telecom",
"com.android.server.telecom.components.TelecomService");
private static final String SERVICE_ACTION = "com.android.ITelecomService";
// 当前系统启动的阶段
@Override
public void onBootPhase(int phase) {
if (phase == PHASE_ACTIVITY_MANAGER_READY) {
...
connectToTelecom();
}
}
//绑定Telecom服务
private void connectToTelecom() {
synchronized (mLock) {
if (mServiceConnection != null) {
// TODO: Is unbinding worth doing or wait for system to rebind?
mContext.unbindService(mServiceConnection);
mServiceConnection = null;
}
TelecomServiceConnection serviceConnection = new TelecomServiceConnection();
Intent intent = new Intent(SERVICE_ACTION);
intent.setComponent(SERVICE_COMPONENT);
int flags = Context.BIND_IMPORTANT | Context.BIND_FOREGROUND_SERVICE
| Context.BIND_AUTO_CREATE;
// Bind to Telecom and register the service
if (mContext.bindServiceAsUser(intent, serviceConnection, flags, UserHandle.SYSTEM)) {
mServiceConnection = serviceConnection;
}
}
}
}

· /packages/services/Telecomm/src/com/android/server/telecom/components/TelecomService.java
绑定服务时,调用 TelecomService的onBind 接口,对整个 Telecom 系统进行初始化,返回 IBinder 接口。
/**
* Implementation of the ITelecom interface.
*/
public class TelecomService extends Service implements TelecomSystem.Component {

@Override
public IBinder onBind(Intent intent) {
Log.d(this, "onBind");
return new ITelecomLoader.Stub() {
@Override
public ITelecomService createTelecomService(IInternalServiceRetriever retriever) {
InternalServiceRetrieverAdapter adapter =
new InternalServiceRetrieverAdapter(retriever);
// 初始化整个 Telecom 系统
initializeTelecomSystem(TelecomService.this, adapter);
//返回IBinder接口
synchronized (getTelecomSystem().getLock()) {
return getTelecomSystem().getTelecomServiceImpl().getBinder();
}
}
};
}
}

Telecom 系统初始化,主要是新建一个 TelecomSystem 的类,在这个类中,会对 Telecom 服务的相关类都初始化。

/**
* This method is to be called by components (Activitys, Services, ...) to initialize the
* Telecom singleton. It should only be called on the main thread. As such, it is atomic
* and needs no synchronization -- it will either perform its initialization, after which
* the {@link TelecomSystem#getInstance()} will be initialized, or some other invocation of
* this method on the main thread will have happened strictly prior to it, and this method
* will be a benign no-op.
*
* @param context
*/
static void initializeTelecomSystem(Context context,
InternalServiceRetrieverAdapter internalServiceRetriever) {
if (TelecomSystem.getInstance() == null) {
NotificationChannelManager notificationChannelManager =
new NotificationChannelManager();
notificationChannelManager.createChannels(context);
// 新建一个单例模式的 TelecomSystem 对象
TelecomSystem.setInstance(
new TelecomSystem(...));
}
}

 /packages/services/Telecomm/src/com/android/server/telecom/TelecomSystem.java
构造一个单例 TelecomSystem 对象,会创建通话有关的类。比如:
· CallsManager· CallIntentProcessor· TelecomServiceImpl

四、构建 VOIP 通话应用

4.1  清单声明和权限

声明一项服务,该服务指定用于在您的应用中实现 ConnectionService 类的类。Telecom 子系统要求该服务声明 BIND_TELECOM_CONNECTION_SERVICE 权限,才能与之绑定。以下示例展示了如何在应用清单中声明该服务:

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

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

<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
4.2 实现连接服务

您的通话应用必须提供 Telecom 子系统能够与之绑定的 ConnectionService 类的实现。示例如下:

public class MyConnectionService extends ConnectionService {
@Override
public void onCreate() {
super.onCreate();
}

@Override
public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) {
super.onCreateIncomingConnection(connectionManagerPhoneAccount, request);
MyConnection conn = new MyConnection(getApplicationContext());
conn.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
conn.setCallerDisplayName("Telecom", TelecomManager.PRESENTATION_ALLOWED);
conn.setAddress(Uri.parse("tel:" + "10086"), TelecomManager.PRESENTATION_ALLOWED);
conn.setRinging();
conn.setInitializing();
conn.setActive();
return conn;
}

@Override
public Connection onCreateOutgoingConnection(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) {
return super.onCreateOutgoingConnection(connectionManagerPhoneAccount, request);
}
}
4.3 实现连接

您的应用应创建 Connection 的子类以表示应用中的来电。示例如下:

public class MyConnection extends Connection {

    MyConnection() {
    }

    /**
     * Telecom 子系统会在您添加新的来电时调用此方法,并且您的应用应显示其来电界面。
     */
    @Override
    public void onShowIncomingCallUi() {
        super.onShowIncomingCallUi();
        // 这里展示您自定义的通话界面
    }
}

五、处理常见的通话场景

以 VOIP 通话来电为例,按以下步骤操作:
1.  注册 PhoneAccount(PhoneAccount 是用来接打电话的,使用 TelecomManager 创建一个 PhoneAccount。PhoneAccount 有唯一的标识叫做 PhoneAccountHandle,Telecom 会通过 PhoneAccountHandle 所提供的 ConnectionService 信息来与 App 进行通信),如下所示:

TelecomManager tm = (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(
new ComponentName(this.getApplicationContext(), MyConnectionService.class),
"AppName");
PhoneAccount phoneAccount = PhoneAccount.builder(phoneAccountHandle, "AppName").setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED).build();
telecomManager.registerPhoneAccount(phoneAccount);

2.  使用 addNewIncomingCall(PhoneAccountHandle, Bundle)  方法在有新来电时告知 Telecom 子系统。

3. Telecom 子系统绑定您应用的ConnectionService 实现,使用onCreateIncomingConnection (PhoneAccountHandle,ConnectionRequest) 方法请求表示新来电的 Connection 类的新实例。 

4.  Telecom 子系统会使用 onShowIncomingCallUi() 方法告知您的应用应显示其来电界面。

结语

Android 10 及更高版本上的用户,接收 IM 即时通讯消息只能通过弹出通知栏的方式,造成用户体验较差。通过本文的介绍,希望对 Android 开发者有所帮助,从而改善用户体验。

       

标签: