2. 实现音频路由自动切换与手动选择
任务说明
在Android音频系统中,音频路由决定了音频流从哪个输出设备播放(如扬声器、有线耳机、蓝牙耳机)。本案例实现一个音频路由管理模块,支持:
- 监听音频设备变化(耳机插拔、蓝牙连接),自动将音频路由切换到当前可用且优先级最高的输出设备;
- 提供手动选择输出设备的能力,并保证手动选择在设备可用期间不被自动切换覆盖。
核心实现
// AudioRouterManager.java
package com.example.audiorouter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.util.concurrent.ConcurrentHashMap;
public class AudioRouterManager {
private static final String TAG = "AudioRouterManager";
private final Context context;
private final AudioManager audioManager;
private final Handler mainHandler = new Handler(Looper.getMainLooper());
// 设备优先级:蓝牙耳机 > 有线耳机 > 扬声器
private static final int[] PRIORITY_ORDER = {
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
AudioDeviceInfo.TYPE_WIRED_HEADSET,
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
};
// 手动选择锁定:key为设备类型,value为是否锁定
private final ConcurrentHashMap<Integer, Boolean> manualLockMap = new ConcurrentHashMap<>();
// 当前路由状态回调
public interface OnRouteChangedListener {
void onRouteChanged(int deviceType, String deviceName);
}
private OnRouteChangedListener routeListener;
public AudioRouterManager(Context context) {
this.context = context.getApplicationContext();
this.audioManager = (AudioManager) this.context.getSystemService(Context.AUDIO_SERVICE);
}
// ========== 对外API ==========
/** 启动监听,注册设备回调与广播 */
public void startListening(OnRouteChangedListener listener) {
this.routeListener = listener;
registerDeviceCallback();
registerWiredHeadsetReceiver();
// 启动时执行一次自动路由
autoRoute();
}
/** 停止监听,释放资源 */
public void stopListening() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
audioManager.unregisterAudioDeviceCallback(deviceCallback);
}
try {
context.unregisterReceiver(wiredHeadsetReceiver);
} catch (IllegalArgumentException e) {
// 未注册时忽略
}
routeListener = null;
}
/** 手动选择输出设备 */
public void manualSelectDevice(int deviceType) {
AudioDeviceInfo target = findDeviceByType(deviceType);
if (target == null) {
Log.w(TAG, "手动选择失败:设备类型 " + deviceType + " 当前不可用");
return;
}
// 锁定该设备类型,防止自动切换覆盖
manualLockMap.put(deviceType, true);
// 清除其他设备类型的锁定
for (int type : PRIORITY_ORDER) {
if (type != deviceType) {
manualLockMap.put(type, false);
}
}
routeToDevice(target);
}
/** 获取当前实际路由设备类型 */
public int getCurrentRouteDeviceType() {
AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
for (AudioDeviceInfo device : devices) {
if (device.isSink()) {
// 检查是否为当前路由设备(通过音频模式简化判断)
if (audioManager.getMode() == AudioManager.MODE_NORMAL) {
return device.getType();
}
}
}
return AudioDeviceInfo.TYPE_UNKNOWN;
}
// ========== 内部实现 ==========
private void registerDeviceCallback() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
audioManager.registerAudioDeviceCallback(deviceCallback, mainHandler);
}
}
private final AudioDeviceCallback deviceCallback = new AudioDeviceCallback() {
@Override
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
autoRoute();
}
@Override
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
// 设备移除时清除对应锁定
for (AudioDeviceInfo device : removedDevices) {
manualLockMap.put(device.getType(), false);
}
autoRoute();
}
};
private final BroadcastReceiver wiredHeadsetReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (AudioManager.ACTION_HEADSET_PLUG.equals(intent.getAction())) {
int state = intent.getIntExtra("state", -1);
if (state == 0) {
// 耳机拔出,清除有线耳机锁定
manualLockMap.put(AudioDeviceInfo.TYPE_WIRED_HEADSET, false);
manualLockMap.put(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, false);
}
autoRoute();
}
}
};
private void registerWiredHeadsetReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(AudioManager.ACTION_HEADSET_PLUG);
context.registerReceiver(wiredHeadsetReceiver, filter);
}
/** 自动路由:选择当前可用且优先级最高的设备 */
private void autoRoute() {
AudioDeviceInfo[] availableDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
for (int deviceType : PRIORITY_ORDER) {
// 如果该设备类型被手动锁定,跳过自动切换
if (Boolean.TRUE.equals(manualLockMap.get(deviceType))) {
continue;
}
AudioDeviceInfo target = findDeviceInArray(availableDevices, deviceType);
if (target != null) {
routeToDevice(target);
return;
}
}
// 无可用设备时回退到扬声器(系统默认)
AudioDeviceInfo speaker = findDeviceByType(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
if (speaker != null) {
routeToDevice(speaker);
}
}
private AudioDeviceInfo findDeviceByType(int deviceType) {
AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
return findDeviceInArray(devices, deviceType);
}
private AudioDeviceInfo findDeviceInArray(AudioDeviceInfo[] devices, int deviceType) {
for (AudioDeviceInfo device : devices) {
if (device.getType() == deviceType && device.isSink()) {
return device;
}
}
return null;
}
private void routeToDevice(AudioDeviceInfo device) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12+ 使用 setPreferredDevice
audioManager.setCommunicationDevice(device);
} else {
// 旧版本通过路由策略设置
int route = mapDeviceTypeToRoute(device.getType());
audioManager.setSpeakerphoneOn(route == AudioManager.ROUTE_SPEAKER);
audioManager.setBluetoothA2dpOn(route == AudioManager.ROUTE_BLUETOOTH_A2DP);
audioManager.setWiredHeadsetOn(route == AudioManager.ROUTE_WIRED_HEADSET);
}
Log.i(TAG, "路由切换到: " + device.getProductName() + " (type=" + device.getType() + ")");
if (routeListener != null) {
routeListener.onRouteChanged(device.getType(), device.getProductName().toString());
}
}
private int mapDeviceTypeToRoute(int deviceType) {
switch (deviceType) {
case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
return AudioManager.ROUTE_SPEAKER;
case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
return AudioManager.ROUTE_BLUETOOTH_A2DP;
case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
case AudioDeviceInfo.TYPE_WIRED_HEADSET:
return AudioManager.ROUTE_WIRED_HEADSET;
default:
return AudioManager.ROUTE_SPEAKER;
}
}
}
使用示例
// MainActivity.java 中调用
AudioRouterManager routerManager = new AudioRouterManager(this);
// 启动监听,自动路由生效
routerManager.startListening((deviceType, deviceName) ->
Log.d("MainActivity", "路由变化: " + deviceName));
// 用户手动选择蓝牙耳机
routerManager.manualSelectDevice(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
// 用户手动选择扬声器
routerManager.manualSelectDevice(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
// 停止监听
routerManager.stopListening();
验证方法
验证路径:在Android 12+真机上运行,依次执行以下操作并观察日志:
- 启动应用,默认路由应为扬声器,日志输出
路由切换到: 扬声器; - 插入有线耳机,日志输出
路由切换到: 有线耳机,声音从耳机输出; - 连接蓝牙耳机,日志输出
路由切换到: 蓝牙耳机,声音从蓝牙输出; - 拔掉蓝牙耳机,日志输出
路由切换到: 有线耳机(若仍插着)或扬声器; - 调用
manualSelectDevice(TYPE_BUILTIN_SPEAKER)后,即使插入耳机,路由仍保持扬声器,日志无切换输出; - 拔掉再插回耳机,自动路由恢复,日志输出切换到耳机。
判定标准:步骤1-4验证自动切换逻辑正确;步骤5验证手动选择锁定生效;步骤6验证设备移除后锁定清除、自动路由恢复。所有日志输出与操作一一对应即通过。
关键说明
- 本实现使用
AudioDeviceCallback监听蓝牙设备变化,使用ACTION_HEADSET_PLUG广播监听有线耳机插拔,两者覆盖了主要路由切换场景; - 手动选择通过
manualLockMap实现锁定,被锁定的设备类型在自动路由时被跳过; - Android 12+使用
setCommunicationDevice,旧版本使用setSpeakerphoneOn等传统API,代码已做版本适配; - 本案例聚焦路由切换逻辑,未包含UI界面,实际集成时可在界面中调用
manualSelectDevice。