2. 实现音频路由自动切换与手动选择

任务说明

在Android音频系统中,音频路由决定了音频流从哪个输出设备播放(如扬声器、有线耳机、蓝牙耳机)。本案例实现一个音频路由管理模块,支持:

  1. 监听音频设备变化(耳机插拔、蓝牙连接),自动将音频路由切换到当前可用且优先级最高的输出设备;
  2. 提供手动选择输出设备的能力,并保证手动选择在设备可用期间不被自动切换覆盖。

核心实现

// 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+真机上运行,依次执行以下操作并观察日志:

  1. 启动应用,默认路由应为扬声器,日志输出路由切换到: 扬声器;
  2. 插入有线耳机,日志输出路由切换到: 有线耳机,声音从耳机输出;
  3. 连接蓝牙耳机,日志输出路由切换到: 蓝牙耳机,声音从蓝牙输出;
  4. 拔掉蓝牙耳机,日志输出路由切换到: 有线耳机(若仍插着)或扬声器;
  5. 调用manualSelectDevice(TYPE_BUILTIN_SPEAKER)后,即使插入耳机,路由仍保持扬声器,日志无切换输出;
  6. 拔掉再插回耳机,自动路由恢复,日志输出切换到耳机。

判定标准:步骤1-4验证自动切换逻辑正确;步骤5验证手动选择锁定生效;步骤6验证设备移除后锁定清除、自动路由恢复。所有日志输出与操作一一对应即通过。

关键说明

  • 本实现使用AudioDeviceCallback监听蓝牙设备变化,使用ACTION_HEADSET_PLUG广播监听有线耳机插拔,两者覆盖了主要路由切换场景;
  • 手动选择通过manualLockMap实现锁定,被锁定的设备类型在自动路由时被跳过;
  • Android 12+使用setCommunicationDevice,旧版本使用setSpeakerphoneOn等传统API,代码已做版本适配;
  • 本案例聚焦路由切换逻辑,未包含UI界面,实际集成时可在界面中调用manualSelectDevice。
音频路由自动切换与手动选择状态机 音频输出设备 扬声器 (SPEAKER) 有线耳机 (WIRED) 蓝牙耳机 (A2DP) 事件监听 AudioDeviceCallback HEADSET_PLUG 广播 手动选择调用 路由决策核心 优先级遍历 手动锁定检查 设备可用性判断 路由执行 setCommunicationDevice 或传统API 回调通知 路由状态流转 扬声器 默认状态 有线耳机 优先级2 蓝牙耳机 优先级1 手动锁定 用户指定设备 耳机插入 蓝牙连接 手动选择 设备移除/解锁后自动回退 自动路由 手动选择