系统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()` 确认会话是否仍处于激活状态 。

  1. **执行控制器的切换**:

比较新选出的目标控制器与当前正在使用的控制器 (`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());

}

```

* **如果目标相同**:通常只需确保回调已注册,并可能根据新的控制器列表进行一些内部状态刷新。

  1. **处理"无活跃会话"的边界情况**:

如果 `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 媒体框架在多任务环境下的协同管理能力。