系统UI客户端(如通知栏媒体控制器、锁屏控件、车载系统等)在处理多个 `MediaSession` 并发的动态变更场景

系统UI客户端(如通知栏媒体控制器、锁屏控件、车载系统等)在处理多个 `MediaSession` 并发的动态变更场景

系统UI客户端(如通知栏媒体控制器、锁屏控件、车载系统等)在处理多个 `MediaSession` 并发的动态变更场景时,其核心任务是**实时追踪“当前应受控的会话”的转移**,并平滑地将用户操作指令路由到正确的目标。这一过程依赖于 `MediaSessionManager` 提供的监听机制与会话状态感知。

### **一、变更监听机制的建立**

UI客户端首先需要向系统注册一个监听器,以接收活跃会话列表变化的通知。这是通过 `MediaSessionManager.addOnActiveSessionsChangedListener()` 方法实现的。该方法内部调用了 `ISessionManager.addSessionsListener` ,将监听器注册到 `MediaSessionManagerService`。

```java
// 系统UI组件(如一个Service)中注册会话变更监听器
public class GlobalMediaControllerService extends Service {
private MediaSessionManager mSessionManager;
private MediaSessionManager.OnActiveSessionsChangedListener mSessionsChangedListener;
private MediaController mCurrentController;

@Override
public void onCreate() {
super.onCreate();
mSessionManager = (MediaSessionManager) getSystemService(Context.MEDIA_SESSION_SERVICE);

// 定义会话变更回调
mSessionsChangedListener = new MediaSessionManager.OnActiveSessionsChangedListener() {
@Override
public void onActiveSessionsChanged(List<MediaController> controllers) {
// 当系统活跃会话列表发生变化时,此方法被回调
handleActiveSessionsChanged(controllers);
}
};

// 注册监听器。第一个参数为监听器对象,第二个参数为ComponentName,通常为null表示监听所有应用。
// 需要声明 android.permission.MEDIA_CONTENT_CONTROL 权限。
ComponentName notificationListener = new ComponentName(this, MyNotificationListenerService.class);
mSessionManager.addOnActiveSessionsChangedListener(mSessionsChangedListener, notificationListener);
}

private void handleActiveSessionsChanged(List<MediaController> newControllers) {
// 此处实现会话切换的核心逻辑
// ...
}

@Override
public void onDestroy() {
if (mSessionManager != null && mSessionsChangedListener != null) {
mSessionManager.removeOnActiveSessionsChangedListener(mSessionsChangedListener);
}
super.onDestroy();
}
}
```

**关键点**:
* **权限要求**:调用 `addOnActiveSessionsChangedListener` 通常需要 `android.permission.MEDIA_CONTENT_CONTROL` 权限,该权限为系统级或签名级权限,普通应用无法获取。因此,此机制主要供系统UI或具有特殊权限的系统组件使用。
* **ComponentName 参数**:该参数用于指定一个 `NotificationListenerService` 的组件。这是Android安全模型的一部分,确保只有用户明确授权可以访问通知的组件才能监听媒体会话变更。系统UI自身具备此条件。

### **二、变更处理的核心逻辑 (`handleActiveSessionsChanged`)**

当 `onActiveSessionsChanged` 被触发时,UI客户端需要执行以下步骤来更新其控制目标:

1. **确定新的目标会话**:
从传入的新控制器列表 `newControllers` 中,根据既定策略选出最合适的 `MediaController`。策略与初始选择类似,但需考虑平滑过渡:
* **优先级**:`PlaybackState.STATE_PLAYING` > `PlaybackState.STATE_PAUSED` > 其他状态。
* **时间戳**:当多个会话状态相同时(例如都处于暂停状态),可比较 `PlaybackState` 中的 `getLastPositionUpdateTime()` 或通过其他上下文信息选择最近活跃的一个。
* **会话活性**:通过 `mediaSession.isActive()` 确认会话是否仍处于激活状态 。

2. **执行控制器的切换**:
比较新选出的目标控制器与当前正在使用的控制器 (`mCurrentController`)。
* **如果目标不同**:需要执行切换操作。
a. **解绑旧控制器**:注销对旧控制器 `Callback` 的注册,避免收到过时的状态更新。
```java
if (mCurrentController != null) {
mCurrentController.unregisterCallback(mMediaControllerCallback);
}
```
b. **绑定新控制器**:注册新控制器的 `Callback`,并立即获取其当前状态(元数据、播放状态等)以更新UI。
```java
mCurrentController = targetController;
if (mCurrentController != null) {
mCurrentController.registerCallback(mMediaControllerCallback);
// 立即同步一次状态
updateUIWithState(mCurrentController.getPlaybackState(),
mCurrentController.getMetadata());
}
```
* **如果目标相同**:通常只需确保回调已注册,并可能根据新的控制器列表进行一些内部状态刷新。

3. **处理“无活跃会话”的边界情况**:
如果 `newControllers` 列表为空,意味着当前没有活跃的媒体会话。UI客户端应:
* 将 `mCurrentController` 置为 `null`。
* 注销所有之前的回调。
* 将UI更新为“无媒体播放”的默认状态(例如,隐藏控制器或显示占位符)。

### **三、`MediaController.Callback` 在会话变更中的持续作用**

即使在会话切换期间,`MediaController.Callback` 也至关重要。它不仅用于接收状态更新,还能帮助验证会话的有效性。

* **状态同步**:`onPlaybackStateChanged`, `onMetadataChanged` 等回调确保UI与远程会话的状态保持同步 。
* **会话失效检测**:`onSessionDestroyed()` 回调是一个关键信号。如果当前控制的会话被应用主动销毁(例如应用退出),此回调会被触发。UI客户端应在此回调中将 `mCurrentController` 置为 `null`,并尝试从最新的活跃会话列表中寻找新的控制目标(可以手动调用 `getActiveSessions` 或等待下一次 `onActiveSessionsChanged` 回调)。

### **四、典型变更场景的流程推演**

| 场景 | 系统触发 `onActiveSessionsChanged` | UI客户端处理逻辑 |
| :--- | :--- | :--- |
| **应用A开始播放(新建会话)** | 列表新增A的控制器。 | 列表中有播放状态的A,故切换至A。绑定A的控制器,更新UI显示A的曲目和播放状态。 |
| **应用A暂停,应用B开始播放** | 列表包含A(暂停)和B(播放)。A的状态已更新。 | 根据策略,选择状态为播放的B。从A切换到B。UI显示B的曲目和播放状态。 |
| **用户清除B的通知或强制停止B** | 列表移除B的控制器。 | 当前控制器B变为无效。在回调或下一次变更中,检测到B不在列表。回退到列表中下一个优先级的会话(如暂停的A),或无会话状态。 |
| **车载系统连接,系统媒体路由变更** | 可能伴随音频路由变化,触发会话列表刷新或优先级重排。 | 处理逻辑不变:依据最新的列表和状态重新选择目标会话。可能因音频焦点转移,原先播放的会话变为暂停,导致控制权切换。 |

### **五、代码示例:一个简化的变更处理器**

```java
private void handleActiveSessionsChanged(List<MediaController> newControllers) {
MediaController newTarget = selectTargetController(newControllers);

// 检查目标是否真的发生了变化
if (mCurrentController != newTarget &&
(mCurrentController == null || newTarget == null ||
!mCurrentController.getSessionToken().equals(newTarget.getSessionToken()))) {

// 解绑旧的
if (mCurrentController != null) {
mCurrentController.unregisterCallback(mControllerCallback);
mCurrentController = null;
}

// 绑定新的
mCurrentController = newTarget;
if (mCurrentController != null) {
mCurrentController.registerCallback(mControllerCallback);
// 立即拉取一次状态,避免等待回调的延迟
mHandler.post(() -> {
if (mCurrentController != null) {
syncControllerState(mCurrentController);
}
});
}

// 通知UI层控制器已变更
notifyControllerChanged(mCurrentController);
} else if (mCurrentController == newTarget && mCurrentController != null) {
// 目标未变,但列表已刷新,确保回调注册并同步一次状态
syncControllerState(mCurrentController);
} else {
// 无目标控制器
clearUI();
}
}

private MediaController selectTargetController(List<MediaController> controllers) {
if (controllers == null || controllers.isEmpty()) {
return null;
}

MediaController playingController = null;
MediaController pausedController = null;
long latestPausedTime = -1;

for (MediaController controller : controllers) {
PlaybackState state = controller.getPlaybackState();
if (state == null) {
continue;
}

int playbackState = state.getState();
if (playbackState == PlaybackState.STATE_PLAYING) {
// 优先返回第一个正在播放的控制器
return controller;
} else if (playbackState == PlaybackState.STATE_PAUSED ||
playbackState == PlaybackState.STATE_BUFFERING) {
// 记录最近暂停的控制器
long updateTime = state.getLastPositionUpdateTime();
if (updateTime > latestPausedTime) {
latestPausedTime = updateTime;
pausedController = controller;
}
}
}

// 如果没有正在播放的,则返回最近暂停的
return pausedController != null ? pausedController : controllers.get(0);
}
```

**总结**:处理 `MediaSession` 变更的核心在于**通过 `MediaSessionManager.OnActiveSessionsChangedListener` 监听系统全局会话列表的动态变化**,并设计一个鲁棒的策略(基于播放状态、时间戳)来从新列表中选出最合适的控制目标。随后,必须**严格管理 `MediaController` 实例的生命周期**,及时注册/注销回调,并同步状态,以确保用户界面始终反映正确的、当前活跃的媒体会话信息,并将控制指令准确送达 。整个过程体现了 Android 媒体框架在多任务环境下的协同管理能力。