You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

蓝牙耳机按键功能修改:自定义BroadcastReceiver失效及锁屏运行需求

解决蓝牙耳机媒体按钮事件接收及锁屏运行问题

你的代码在处理媒体按钮事件时,碰到了Android高版本权限限制、系统广播机制变化以及锁屏进程存活的几个关键问题,下面是一步步的修复方案:

1. 补充适配高版本的必要权限

从Android 12开始,蓝牙权限被拆分,仅声明BLUETOOTH已经不足以获取蓝牙设备控制权;同时Android 13+需要通知权限才能显示前台服务通知。更新你的清单文件:

<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" android:minSdkVersion="33"/>

2. 改用AudioManager注册媒体按钮接收器(Android 5.0+ 强制要求)

从Android 5.0开始,系统不再通过普通广播分发媒体按钮事件,而是优先发送给持有音频焦点或通过AudioManager.registerMediaButtonEventReceiver()注册的应用。普通的registerReceiver()无法稳定接收事件,尤其是后台或锁屏状态下。

我们需要通过前台服务来管理接收器,确保锁屏时进程不会被系统杀死:

步骤2.1 创建前台服务

public class MediaButtonService extends Service {
    private AudioManager mAudioManager;
    private ComponentName mReceiverComponent;
    private MediaButtonsIntentReceiver mReceiver;

    @Override
    public void onCreate() {
        super.onCreate();
        // 初始化音频管理器与接收器
        mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
        mReceiver = new MediaButtonsIntentReceiver();
        mReceiverComponent = new ComponentName(this, MediaButtonsIntentReceiver.class);
        
        // 注册媒体按钮接收器
        mAudioManager.registerMediaButtonEventReceiver(mReceiverComponent);

        // 获取音频焦点(系统分发事件的关键前提)
        AudioAttributes audioAttrs = new AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                .build();
        AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
                .setAudioAttributes(audioAttrs)
                .setOnAudioFocusChangeListener(focusChange -> {
                    // 可在这里处理音频焦点丢失的逻辑
                })
                .build();
        mAudioManager.requestAudioFocus(focusRequest);

        // 启动前台服务,确保锁屏时进程存活
        startForeground(1, createForegroundNotification());
    }

    private Notification createForegroundNotification() {
        // 创建通知渠道(Android 8.0+ 要求)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel("media_btn_channel",
                    "Media Button Handler", NotificationManager.IMPORTANCE_LOW);
            NotificationManager manager = getSystemService(NotificationManager.class);
            manager.createNotificationChannel(channel);
        }

        return new NotificationCompat.Builder(this, "media_btn_channel")
                .setContentTitle("蓝牙按键监听")
                .setContentText("正在监听蓝牙耳机按键")
                .setSmallIcon(R.drawable.ic_notification)
                .setPriority(NotificationCompat.PRIORITY_LOW)
                .build();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // 注销接收器并释放音频焦点
        mAudioManager.unregisterMediaButtonEventReceiver(mReceiverComponent);
        mAudioManager.abandonAudioFocus(null);
        stopForeground(true);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

步骤2.2 优化你的BroadcastReceiver

保留原有逻辑,优化事件判断避免重复触发:

public class MediaButtonsIntentReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (!Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
            return;
        }

        KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
        // 只处理按键按下事件,避免抬起时重复触发
        if (event == null || event.getAction() != KeyEvent.ACTION_DOWN) {
            return;
        }

        switch (event.getKeyCode()) {
            case KeyEvent.KEYCODE_VOLUME_UP:
                Toast.makeText(context, "音量+被按下", Toast.LENGTH_SHORT).show();
                break;
            case KeyEvent.KEYCODE_VOLUME_DOWN:
                Toast.makeText(context, "音量-被按下", Toast.LENGTH_SHORT).show();
                break;
            case KeyEvent.KEYCODE_MEDIA_NEXT:
                Toast.makeText(context, "下一曲被按下", Toast.LENGTH_SHORT).show();
                break;
            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
                Toast.makeText(context, "上一曲被按下", Toast.LENGTH_SHORT).show();
                break;
        }

        abortBroadcast(); // 截断广播,防止其他应用处理该事件
    }
}

步骤2.3 在清单中声明服务

<service android:name=".MediaButtonService"
         android:foregroundServiceType="mediaPlayback"/>

3. 在MainActivity中启动服务并申请动态权限

Android 6.0+的危险权限需要动态申请,所以启动服务前要先完成权限校验:

public class MainActivity extends AppCompatActivity {
    private static final int PERMISSION_REQUEST_CODE = 100;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (hasRequiredPermissions()) {
            startMediaButtonService();
        } else {
            requestRequiredPermissions();
        }
    }

    private boolean hasRequiredPermissions() {
        boolean hasBtConnect = ContextCompat.checkSelfPermission(this,
                Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED;
        boolean hasNotificationPerm = true;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            hasNotificationPerm = ContextCompat.checkSelfPermission(this,
                    Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED;
        }
        return hasBtConnect && hasNotificationPerm;
    }

    private void requestRequiredPermissions() {
        List<String> permissions = new ArrayList<>();
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
            permissions.add(Manifest.permission.BLUETOOTH_CONNECT);
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
                ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
            permissions.add(Manifest.permission.POST_NOTIFICATIONS);
        }
        ActivityCompat.requestPermissions(this, permissions.toArray(new String[0]), PERMISSION_REQUEST_CODE);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == PERMISSION_REQUEST_CODE) {
            if (hasRequiredPermissions()) {
                startMediaButtonService();
            } else {
                Toast.makeText(this, "需要授予权限才能监听蓝牙按键", Toast.LENGTH_LONG).show();
            }
        }
    }

    private void startMediaButtonService() {
        Intent serviceIntent = new Intent(this, MediaButtonService.class);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            startForegroundService(serviceIntent);
        } else {
            startService(serviceIntent);
        }
    }
}

4. 移除静态注册的广播接收器

由于我们已经通过服务动态注册了媒体按钮接收器,清单中静态注册的<receiver>可以删除——Android 8.0+对隐式广播的静态注册有严格限制,它无法在后台正常工作,还可能和动态注册冲突。

关键注意点

  • 前台服务的必要性:锁屏状态下系统会限制后台进程活动,前台服务能让应用保持活跃,确保持续接收按键事件。
  • 音频焦点:获取音频焦点是系统分发媒体按钮事件的核心前提,没有焦点的应用几乎无法收到事件。
  • 版本适配:不同Android版本的权限要求差异较大,一定要做好版本判断与适配。

内容的提问来源于stack exchange,提问作者user9287834

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 06:38:01