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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值