The code for handling media buttons from headsets that I use in my Text-to-Speech app works great under Android API 22 through 25 (in older versions of Android they are handled by other, now depreciated means). However under Android 8 "Oreo", both public beta and final release, it does not work. Here is the relevant code:
When the service starts, I create MediaSessionCompact object:
mSession = new MediaSessionCompat(getApplicationContext(), "my.package.name._player_session");
mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mSession.setActive(true);
mSession.setCallback(myMediaSessionCallback);
PlaybackStateCompat state = new PlaybackStateCompat.Builder()
.setActions(ACTION_PLAY_PAUSE | ACTION_PLAY | ACTION_PAUSE |
ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS |
ACTION_FAST_FORWARD | ACTION_REWIND
)
.setState(PlaybackStateCompat.STATE_PAUSED, 0 /*PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN*/, 1f)
.build();
mSession.setPlaybackState(state);
There is of course session media callback defined:
private MediaSessionCompat.Callback myMediaSessionCallback = new MediaSessionCompat.Callback() {
#Override
public boolean onMediaButtonEvent(Intent mediaButtonIntent) {
// The log output below never appears on "Oreo", nothing comes here.
Log.d(TAG, "callback onMediaButtonEvent() Compat");
MediaButtonIntentReceiver.handleIntent(mediaButtonIntent.getAction(), (KeyEvent) mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT));
return true;
}
#Override
public void onSkipToNext() {
//...
}
// etc. other overrides
};
I also experimented with PendingIntent, using MediaButtonReceiver.buildMediaButtonPendingIntent() and set mSession.setMediaButtonReceiver(pendingIntent) for all the actions I'm interested in, then in my service onStartCommand() I call MediaButtonReceiver.handleIntent(mSession, intent):
// still in the same service:
mSession.setMediaButtonReceiver(
MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
mMediaButtonReceiverComponentName,
ACTION_PLAY));
mSession.setMediaButtonReceiver(
MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
mMediaButtonReceiverComponentName,
ACTION_PAUSE));
mSession.setMediaButtonReceiver(
MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
mMediaButtonReceiverComponentName,
ACTION_PLAY_PAUSE));
and in the service onStartCommand():
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
// ...
if (intent != null) {
MediaButtonReceiver.handleIntent(mSession, intent);
// ...
}
return START_NOT_STICKY;
}
Nothing, it's completely dumb to media buttons press events. What's wrong with "O" or my code there??? I'm completely baffled.
Update 8/32/2017
I also created a trivial but working app project that demonstrates the problem, please see: https://github.com/gregko/PlayerServiceSample. This project displays LogCat output when a media button is pressed on a headset under Android 5.x to 7.x, but fails completely under Android 8 "Oreo".
Update 9/1/2017
There is now an open issue on Android Issue Tracker about this, which I submitted, at https://issuetracker.google.com/issues/65175978. Still the media buttons work in several music player apps I tested on Oreo, I just can't figure out what do they do differently to make them work... The context of my app is not playing music, but reading aloud text with Text to Speech service, so a lot of code from Music Player examples does not apply.
Solved. On "Android 8.0 Behavior Changes" Google page we find this text:
In Android 8.0 (API level 26) the handling of media button events is different:
The handling of media buttons in a UI activity has not changed: foreground activities still get priority in handling media button events.
If the foreground activity does not handle the media button event, the system routes the event to the app that most recently played audio locally. The active status, flags, and playback state of a media session are not considered when determining which app receives media button events.
If the app's media session has been released, the system sends the media button event to the app's MediaButtonReceiver if it has one.
For every other case, the system discards the media button event.
All I had to do to make my trivial sample work was to play some sound with MediaPlayer. Apparently playing sound with Text-to-Speech API does not qualify, which in my opinion is a bug.
Here is the code I added to my trivial sample to make it work, playing a very brief and silent WAV file from Raw resources directory:
final MediaPlayer mMediaPlayer;
mMediaPlayer = MediaPlayer.create(this, R.raw.silent_sound);
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
#Override
public void onCompletion(MediaPlayer mediaPlayer) {
mMediaPlayer.release();
}
});
mMediaPlayer.start();
Update
Submitted the bug report to Android issue tracker at https://issuetracker.google.com/issues/65344811
Update 2, Oct. 10, 2017
Google now says that Oreo behavior in this respect is "by design" and won't fix it. Read the reply near the end of the issue tracker post above. I must say I'm disappointed.
Related
I've been working on a navigation feature for a maps app which has voice instructions. The problem is that when announcing the instructions, the first 500 milliseconds of the instruction gets cut off. For eg, if the instruction is "In 200m turn right", in the bluetooth earphone it ends up sounding like "200m turn right". Or if the instruction is "Continue for 2 kilometers", then it sounds like "tinue for 2 kilometers".
This is the code I'm using for TTS -
//Initialisation happens only once
var textToSpeechEngine = TextToSpeech(this) { status ->
if (status == TextToSpeech.SUCCESS) {
textToSpeechEngine?.language = Locale.ENGLISH
textToSpeechEngine?.setSpeechRate(0.8f)
}
}
...
//When text to speak is ready, invoking the speak method
textToSpeechEngine?.speak(textToSpeak, TextToSpeech.QUEUE_FLUSH, null, "tts1")
Additionally, I'm also using AudioFocusRequest to request and abandon focus so that any music playing in the background lowers its volume while the instruction is being spoken. This is the code I'm using for that -
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
var focusRequest: AudioFocusRequest? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build())
.setAcceptsDelayedFocusGain(false)
.build()
audioManager.requestAudioFocus(focusRequest)
} else {
audioManager.requestAudioFocus(null, AudioManager.STREAM_NOTIFICATION, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
}
textToSpeechEngine?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
fun abandonFocus() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
focusRequest?.let { request -> audioManager.abandonAudioFocusRequest(request) }
} else {
audioManager.abandonAudioFocus(null)
}
}
override fun onStart(utteranceId: String?) {}
override fun onDone(utteranceId: String?) {
abandonFocus()
}
override fun onError(utteranceId: String?) {
abandonFocus()
}
})
Point to note here is that this issue is not happening in all the bluetooth devices I've tested with. The issue happened with 2 Chinese brands bluetooth earphones but is not happening with a bluetooth Sony over-the-head headset. Also, the issue does not happen if music is playing in the background while navigation is going on.
As I understand it, it seems that the cheaper bluetooth earphones seem to keep the "connection alive" only when audio is actively coming through else it stops the connection temporarily in order to save battery I guess? However, when music is playing in the background, the connection is kept alive constantly so the instruction speech does not get cut off.
What can I do to fix this or work around it?
note that requestAudioFocus may take a OnAudioFocusChangeListener as first param, you are passing null in there (also Builder have this param). switching focus may take some (short) time, so I would recomend to fire your TTS when you get this callback fired with AUDIOFOCUS_GAIN
Assuming that you're correct that it is purely hardware at fault (which is what it seems like):
You can use the playSilence() or playSilentUtterance() methods of the TextToSpeech class to play silence for 500ms prior to your main speak() command... which should fool the speakers/headphones.
It may help to also use QUEUE_ADD instead of QUEUE_FLUSH for your main speak() request to make sure that it it attached the the previous (silence) with no gap... and that it doesn't prematurely end the first (silent) utterance.
There are a lot of variables at play here. You could consider:
Can you find an app that uses TextToSpeech which DOES work correctly even on these problematic devices? If so, then the problem must be solvable in code, and maybe you could find the source code for the app you tested and look at how they are setting up the AudioManager.
I'm implementing cast feature in one of my application and trying to mute the audio being cast from sender application to default receiver app.
Using "setStreamVolume()" api on RemoteMediaPlayer object to mute the audio, see below code:
remoteMediaPlayer.setStreamVolume(googleApiClient, 0).setResultCallback(
new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
#Override
public void onResult( MediaChannelResult result) {
Status status = result.getStatus();
Log.d(TAG, "MUTE status:"+status);
}
});
But observed that, its returning 2100 (STATUS_FAILED) status code in result callback and not able mute the audio in receiver application.
Please suggest...
I strongly suggest not using the Stream Volume but rather use the Device Volume. The corresponding mute method can be found here.
I've been running into an issue with the MediaPlayer on Lollipop devices. Basically when the device screen is off (i.e. user locked the device) the playback continues, but ends about 1 - 2 seconds too early. This doesn't happen when the screen is on though.
I have an onCompletionListener on the MediaPlayer:
#Override
public void onCompletion(final MediaPlayer mediaPlayer) {
int progress = mediaPlayer.getCurrentPosition();
int duration = mediaPlayer.getDuration();
Log.d("PlaybackController", "progress: " + progress + " duration: " + duration);
Log.d("PlaybackController", "Delay: " + (duration - progress)); // I'm seeing a difference of 1 - 3 seconds :(.
mServiceHandler.postDelayed(new Runnable() {
#Override
public void run() {
broadcastCompleted();
}
}, Math.max(duration - progress, 0));
}
This usually prints: Delay: [1500 - 3000]. I was wondering if there was a wake lock I'm missing, but I'm making the correct locks mentioned here: http://developer.android.com/guide/topics/media/mediaplayer.html, which include a PARTIAL_WAKE_LOCK and a WifiLock. Is there something else I'm missing?
Ok it looks like the issue is Android 5.0.1's experimental MediaPlayer called NuPlayer. NuPlayer is being enabled by default on all Android 5.0.1 devices and is only disabled through Developer Options. I've filed a bug against Android here: https://code.google.com/p/android/issues/detail?id=94069&thanks=94069&ts=1420659450
Here's a sample email you can send your users when they face issues with Media playback on Android 5.0.1 devices:
It looks like this might be a bug on Android's new experimental MediaPlayer called NuPlayer. To fix this, please follow these steps:
Go to Android Settings
Go to "About Phone"
Scroll down to "Build Number" and tap the Build number 7 times.
You'll see a message saying "you are now X steps away from being a developer".
After tapping it 7 times it will say "You are now a developer!"
Go back to the main settings screen and you'll see a new option called "Developer Options" right above "About Phone"
Go into Developer Options and Unselect "Use NuPlayer (experimental)" under the Media section.
Update:
Setting a partial wake lock on the MediaPlayer resolves this problem:
playerToPrepare.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
A partial wake lock shouldn't have too big of an impact, and it seems like MediaPlayer itself cleans this up when playback completes.
-- Original Answer ---
I'm cross-posting my answer from here Prevent my audio app using NuPlayer on Android Lollipop 5.x?, until there's a fix out for NuPlayer, the best you can do is to detect when NuPlayer is enabled and take the user to the Developer Settings.
This approach checks Android's system properties values to see if the user have enabled the use of AwesomePlayer or not under Developer Settings. Since Lollipop have NuPlayer on by default, if this value is disabled, we know NuPlayer will be used.
Drop SystemProperties.java into your project for access to read the system properties, do not change its package name from android.os (it calls through to its corresponding JNI methods, so needs to stay the same).
You can now check if the phone is Lollipop/5.0, if AwesomePlayer is enabled, and act accordingly if it's not (e.g. by opening the Developer Settings):
public void openDeveloperSettingsIfAwesomePlayerNotActivated(final Context context) {
final boolean useAwesome = SystemProperties.getBoolean("persist.sys.media.use-awesome", false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !useAwesome) {
final Intent intent = new Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS);
context.startActivity(intent);
}
}
In the documentation, it is said:
The mute command is protected against client process death: if a process with an active mute request on a stream dies, this stream will be unmuted automatically.
The mute requests for a given stream are cumulative: the AudioManager can receive several mute requests from one or more clients and the stream will be unmuted only when the same number of unmute requests are received.
Well, the first paragraph is true; Whenever my process dies, all of the streams I muted are automatically unmuted.
However, no matter how many time I call setStreamMute(someStream, false) it NEVER EVER unmutes.
Last time I tried calling it over 1 million times after muting only ONCE and NOTHING happens!
Just to mention - If i unmute it in the same method I mute it - it stays unmuted. But on the next calls to the same method - it never unmutes.
I am muting in a Broadcast Receiver onReceive method, which I start using an alarm manager. So maybe it because my app was killed during the time between the muting call and the unmuting call? (But my app still stays in the RAM)
Can this problem be because I am not keeping a reference to the AlarmManager (Getting different instances each time?)
Did anyone encounter this problem?
Apperantly, there is a bug in these Android versions; Tested for versions 2.2 and 2.3.3 and the bug exists.
Looks like, if you call setStreamMute on an AudioManager object:
AudioManager am = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
am.setStreamMute(...., true);
and you lose your reference, then get a new reference:
am = null;
am = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
No matter how many times you call am.setStreamMute(..., false) now, it will never unmutes.
I think ill report this bug now..
Lesson: Keep a static reference to your AudioManager.
#Michell Bak, thanks for giving me the idea to check whether its the Android software bug :) I've been stuck on this thing for way too much time, and I never had the idea to see if its not my fault.
I've never used that API before, but a quick Google search, returned a few results with it not working. It seems to be a bug that's still present in Android 2.3:
http://code.google.com/p/android/issues/detail?id=4235
I solve the problem by putting the audio manager variable in the application
public class myApplication extends Application {
static AudioManager am;
public void onCreate() {
super.onCreate();
am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
this.setMute(false);
}
}
Then in my activity class add this function:
private AudioManager getAM() {
return ((elpApplication)getApplication()).am;
}
and here is how I use the getAM();
private void toogleMediaPlayerMute() {
//defaultVolumn = am.getStreamVolume(AudioManager.STREAM_MUSIC);
elpApplication app = getElpApp();
Log.d("appmute", String.valueOf(app.isMute()));
if (!app.isMute()) {
getAM().setStreamVolume(AudioManager.STREAM_MUSIC, 0,
AudioManager.FLAG_PLAY_SOUND);
getAM().setStreamMute(AudioManager.STREAM_MUSIC, true);
ismute = true;
} else {
int maxVolume = getAM().getStreamMaxVolume(AudioManager.STREAM_MUSIC);
Log.d("maxvol", String.valueOf(maxVolume));
getAM().setStreamMute(AudioManager.STREAM_MUSIC, false);
getAM().setStreamVolume(AudioManager.STREAM_MUSIC, maxVolume,
AudioManager.FLAG_SHOW_UI);
ismute = false;
// app.setMute(ismute);
}
app.setMute(ismute);
}
I have been having the same problem w/ newer versions of the API. So, to work [around] this issue, I've implemented a bit of 'old-school' solution. If your design is such that you handle getting the byte stream / sending the byte stream directly - send a byte array filled with zeros if mute is selected:
*
...
byte[] whatToSend = realByteData;
byte [] mutedOutput = new byte[recBuffSize];
Array.setByte( mutedOutput, 0, (byte) 0);
...
if ( muteSet )
whatToSend = mutedOutput;
amountWritten = audioTrack.write(whatToSend, 0, amountRead);
*
I have developed a TTS based application. When playing it sound, if any other notification triggered, then TTS playing with sound breaking. (Ex: if SMS received TTS sound breaks)
What are condition under which such an issue can occur?
there is a bug in the Android API < 8. To restore the original sound, just say "nothing".
protected void speak(final String text, final int mode) {
speechParams.clear();
speechParams.put(TextToSpeech.Engine.KEY_PARAM_STREAM,
String.valueOf(AudioManager.STREAM_NOTIFICATION));
engine.speak(text, TextToSpeech.QUEUE_ADD, speechParams);
//Bug with API level < 8: The original sound isn't restored automatically.
//So we do it by speaking nothing.
speechParams.put(TextToSpeech.Engine.KEY_PARAM_STREAM, String.valueOf(AudioManager.STREAM_MUSIC));
this.engine.speak("", mode, speechParams);
}
}
Hope that helps.