How to receive Bluetooth headset play/pause events when TextToSpeech.speak()?
Asked Answered
O

0

5

I am trying to handle the pause/play button from a Bluetooth headset (e.g. Jabra Elite 7 Pro) by simply logging into Logcat a message whenever it is pressed. The “twist” is that this is during TextToSpeech.speak(...), not during ordinary media playback.

To accomplish that, I introduced the following changes into my app’s code (Java):

1. Updated build.gradle

Added the necessary dependencies in my build.gradle file:

dependencies {
    implementation 'androidx.media:media:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.7.0'
}

2. Updated AndroidManifest.xml

Added the MediaButtonReceiver to my AndroidManifest.xml:

 <receiver android:name="androidx.media.session.MediaButtonReceiver" android:exported="true">
     <intent-filter>
         <action android:name="android.intent.action.MEDIA_BUTTON" />
     </intent-filter>
 </receiver>

and necessary Bluetooth permissions:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

3. Modified My Activity

import android.content.ComponentName;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.session.MediaSession;
import androidx.media.session.MediaButtonReceiver;

import android.content.Intent;
import android.media.session.MediaSession;
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;

along with:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "BluetoothMediaButton";
    private MediaSession mediaSession;
    private AudioFocusRequest audioFocusRequest;
    private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;

and MediaSession.setCallback(..):

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // etc. ...

        AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        audioManager.registerMediaButtonEventReceiver(new ComponentName(getPackageName(), MediaButtonReceiver.class.getName()));
        audioFocusChangeListener = focusChange -> {
            switch (focusChange) {
                case AudioManager.AUDIOFOCUS_GAIN:
                    Log.d(TAG, "Audio focus gained");
                    // Resume playback or increase volume
                    break;
                case AudioManager.AUDIOFOCUS_LOSS:
                    Log.d(TAG, "Audio focus lost");
                    // Stop playback
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                    Log.d(TAG, "Audio focus lost temporarily");
                    // Pause playback or lower volume
                    break;
            }
        };

        Log.d(TAG, "Setting up MediaSession");
        mediaSession = new MediaSession(this, "BluetoothMediaButtonExample");
        Log.d(TAG, "MediaSession created");
        mediaSession.setCallback(new MediaSession.Callback() {
            @Override
            public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
                Log.d(TAG, "onMediaButtonEvent called");
                Log.d(TAG, "Media button pressed: " + mediaButtonIntent.getAction());
                return super.onMediaButtonEvent(mediaButtonIntent);
            }
        });
        Log.d(TAG, "MediaSession callback set");
        mediaSession.setActive(true);
        Log.d(TAG, "MediaSession activated");
        
        // etc. ...
    }

Note: I didn't introduce any “audio focus” code initially. But since I didn’t receive the expected events, I suspected that the media button events may be intercepted by another app that has audio focus.

So I added the audio focus logging above, plus code to explicitly request audio focus:

    private void requestAudioFocus() {
        AudioAttributes playbackAttributes = new AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                .build();

        audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
                .setAudioAttributes(playbackAttributes)
                .setAcceptsDelayedFocusGain(true)
                .setOnAudioFocusChangeListener(audioFocusChangeListener)
                .build();

        int result = mAudioManager.requestAudioFocus(audioFocusRequest);
        if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            Log.d(TAG, "Audio focus request granted");
            // Start playback or enable media button handling
        } else {
            Log.d(TAG, "Audio focus request failed");
        }
    }

    private void abandonAudioFocus() {
        if (audioFocusRequest != null) {
            mAudioManager.abandonAudioFocusRequest(audioFocusRequest);
            Log.d(TAG, "Audio focus abandoned");
        }
    }

and

@Override
public void onResume() {
  super.onResume();
  requestAudioFocus();
}

and

@Override
public void onPause() {
  super.onPause();
  abandonAudioFocus();
}

and

@Override
public void onDestroy() {
  super.onDestroy();
  if (mediaSession != null)
    mediaSession.release();
}

Confirmed by the following Logcat relevant messages (filtered by “media”):

---------------------------- PROCESS STARTED (31873) for package com.me.myapp ----------------------------
09:50:34.384  MyActivity               D  T=31873 Setting up MediaSession | MyActivity.onCreate(MyActivity.java:892) < MyAppActivity.onCreate(MyAppActivity.java:24)
09:50:34.387                           D  T=31873 MediaSession created | MyActivity.onCreate(MyActivity.java:894) < MyAppActivity.onCreate(MyAppActivity.java:24)
09:50:34.387                           D  T=31873 MediaSession callback set | MyActivity.onCreate(MyActivity.java:903) < MyAppActivity.onCreate(MyAppActivity.java:24)
09:50:34.388                           D  T=31873 MediaSession activated | MyActivity.onCreate(MyActivity.java:905) < MyAppActivity.onCreate(MyAppActivity.java:24)
09:50:36.217 cr_media                  W  BLUETOOTH_CONNECT permission is missing.
09:50:36.218                           W  getBluetoothAdapter() requires BLUETOOTH permission
09:50:36.219                           W  registerBluetoothIntentsIfNeeded: Requires BLUETOOTH permission
---------------------------- PROCESS ENDED (31873) for package com.me.myapp ----------------------------

and (filtered by “audio”):

---------------------------- PROCESS STARTED (31873) for package com.me.myapp ----------------------------
09:50:34.698  MyActivity               D  T=31873 Audio focus request granted | MyActivity.requestAudioFocus(MyActivity.java:988) < MyActivity.onResume(MyActivity.java:1099)
09:50:34.800                           D  T=31873 Audio focus abandoned | MyActivity.abandonAudioFocus(MyActivity.java:998) < MyActivity.onPause(MyActivity.java:1651)
09:50:36.239                           D  T=31873 Audio focus request granted | MyActivity.requestAudioFocus(MyActivity.java:988) < MyActivity.onResume(MyActivity.java:1099)
---------------------------- PROCESS ENDED (31873) for package com.me.myapp ----------------------------

Still, despite the explicit request for audio focus, I have not been able to see in Logcat the desired "onMediaButtonEvent called" message.

After much research I discovered that there is a documented “bug” that prevents receiving media button events for TTS: https://issuetracker.google.com/issues/249741615

Currently, the only workaround is "I just played a silent sound for less than 1s using MediaPlayer class and when done I start using TextToSpeech."

This workaround, unfortunately, is not suitable for my app, so I am looking for additional ideas to receive Bluetooth headset play/pause events when TextToSpeech.speak(...).

Is there a way to overcome this audio focus (for TTS) that doesn’t require playing a 1-second .wav file before every speak()?

Odometer answered 17/9, 2024 at 7:51 Comment(1)
I tried the workaround described here and it did log a message in response for the button click, but it is "MediaButtonReceiver D Ignore unsupported intent: Intent { act=android.intent.action.MEDIA_BUTTON flg=0x10". This led me to android.googlesource.com/platform/frameworks/support/+/refs/… where you see the condition for it not be ignored: !intent.hasExtra(Intent.EXTRA_KEY_EVENT). You still need the workaround, though.Tough

© 2022 - 2025 — McMap. All rights reserved.