blob: 5aa3cbbd5db7cb1f6f856e6ce45e3902bf2ecaff [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.bluetooth.a2dpsink;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClientCall;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.media.MediaPlayer;
import android.os.Handler;
import android.os.Message;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import com.android.bluetooth.R;
import com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService;
import com.android.bluetooth.hfpclient.HeadsetClientService;
import java.util.List;
/**
* Bluetooth A2DP SINK Streaming Handler.
*
* This handler defines how the stack behaves once the A2DP connection is established and both
* devices are ready for streaming. For simplification we assume that the connection can either
* stream music immediately (i.e. data packets coming in or have potential to come in) or it cannot
* stream (i.e. Idle and Open states are treated alike). See Fig 4-1 of GAVDP Spec 1.0.
*
* Note: There are several different audio tracks that a connected phone may like to transmit over
* the A2DP stream including Music, Navigation, Assistant, and Notifications. Music is the only
* track that is almost always accompanied with an AVRCP play/pause command.
*
* Streaming is initiated by either an explicit play command from user interaction or audio coming
* from the phone. Streaming is terminated when either the user pauses the audio, the audio stream
* from the phone ends, the phone disconnects, or audio focus is lost. During playback if there is
* a change to audio focus playback may be temporarily paused and then resumed when focus is
* restored.
*/
public class A2dpSinkStreamHandler extends Handler {
private static final String TAG = "A2dpSinkStreamHandler";
private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
// Configuration Variables
private static final int DEFAULT_DUCK_PERCENT = 25;
private static final int SETTLE_TIMEOUT = 400;
// Incoming events.
public static final int SRC_STR_START = 0; // Audio stream from remote device started
public static final int SRC_STR_STOP = 1; // Audio stream from remote device stopped
public static final int SNK_PLAY = 2; // Play command was generated from local device
public static final int SNK_PAUSE = 3; // Pause command was generated from local device
public static final int SRC_PLAY = 4; // Play command was generated from remote device
public static final int SRC_PAUSE = 5; // Pause command was generated from remote device
public static final int DISCONNECT = 6; // Remote device was disconnected
public static final int AUDIO_FOCUS_CHANGE = 7; // Audio focus callback with associated change
public static final int REQUEST_FOCUS = 8; // Request focus when the media service is active
public static final int DELAYED_PAUSE = 9; // If a call just started allow stack time to settle
// Used to indicate focus lost
private static final int STATE_FOCUS_LOST = 0;
// Used to inform bluedroid that focus is granted
private static final int STATE_FOCUS_GRANTED = 1;
// Private variables.
private A2dpSinkService mA2dpSinkService;
private Context mContext;
private AudioManager mAudioManager;
// Keep track if the remote device is providing audio
private boolean mStreamAvailable = false;
private boolean mSentPause = false;
// Keep track of the relevant audio focus (None, Transient, Gain)
private int mAudioFocus = AudioManager.AUDIOFOCUS_NONE;
// In order for Bluetooth to be considered as an audio source capable of receiving media key
// events (In the eyes of MediaSessionService), we need an active MediaPlayer in addition to a
// MediaSession. Because of this, the media player below plays an incredibly short, silent audio
// sample so that MediaSessionService and AudioPlaybackStateMonitor will believe that we're the
// current active player and send the Bluetooth process media events. This allows AVRCP
// controller to create a MediaSession and handle the events if it would like. The player and
// session requirement is a restriction currently imposed by the media framework code and could
// be reconsidered in the future.
private MediaPlayer mMediaPlayer = null;
// Focus changes when we are currently holding focus.
private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
if (DBG) {
Log.d(TAG, "onAudioFocusChangeListener focuschange " + focusChange);
}
A2dpSinkStreamHandler.this.obtainMessage(AUDIO_FOCUS_CHANGE, focusChange)
.sendToTarget();
}
};
public A2dpSinkStreamHandler(A2dpSinkService a2dpSinkService, Context context) {
mA2dpSinkService = a2dpSinkService;
mContext = context;
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
void requestAudioFocus(boolean request) {
obtainMessage(REQUEST_FOCUS, request).sendToTarget();
}
int getFocusState() {
return mAudioFocus;
}
boolean isPlaying() {
return (mStreamAvailable
&& (mAudioFocus == AudioManager.AUDIOFOCUS_GAIN
|| mAudioFocus == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK));
}
@Override
public void handleMessage(Message message) {
if (DBG) {
Log.d(TAG, " process message: " + message.what);
Log.d(TAG, " audioFocus = " + mAudioFocus);
}
switch (message.what) {
case SRC_STR_START:
mStreamAvailable = true;
if (isTvDevice() || shouldRequestFocus()) {
requestAudioFocusIfNone();
}
break;
case SRC_STR_STOP:
// Audio stream has stopped, maintain focus but stop avrcp updates.
break;
case SNK_PLAY:
// Local play command, gain focus and start avrcp updates.
requestAudioFocusIfNone();
break;
case SNK_PAUSE:
mStreamAvailable = false;
// Local pause command, maintain focus but stop avrcp updates.
break;
case SRC_PLAY:
mStreamAvailable = true;
// Remote play command.
if (isIotDevice() || isTvDevice() || shouldRequestFocus()) {
requestAudioFocusIfNone();
break;
}
break;
case SRC_PAUSE:
mStreamAvailable = false;
// Remote pause command, stop avrcp updates.
break;
case REQUEST_FOCUS:
requestAudioFocusIfNone();
break;
case DISCONNECT:
// Remote device has disconnected, restore everything to default state.
mSentPause = false;
break;
case AUDIO_FOCUS_CHANGE:
mAudioFocus = (int) message.obj;
// message.obj is the newly granted audio focus.
switch (mAudioFocus) {
case AudioManager.AUDIOFOCUS_GAIN:
removeMessages(DELAYED_PAUSE);
// Begin playing audio, if we paused the remote, send a play now.
startFluorideStreaming();
if (mSentPause) {
sendAvrcpPlay();
mSentPause = false;
}
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// Make the volume duck.
int duckPercent = mContext.getResources()
.getInteger(R.integer.a2dp_sink_duck_percent);
if (duckPercent < 0 || duckPercent > 100) {
Log.e(TAG, "Invalid duck percent using default.");
duckPercent = DEFAULT_DUCK_PERCENT;
}
float duckRatio = (duckPercent / 100.0f);
if (DBG) {
Log.d(TAG, "Setting reduce gain on transient loss gain=" + duckRatio);
}
setFluorideAudioTrackGain(duckRatio);
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// Temporary loss of focus, if we are actively streaming pause the remote
// and make sure we resume playback when we regain focus.
sendMessageDelayed(obtainMessage(DELAYED_PAUSE), SETTLE_TIMEOUT);
setFluorideAudioTrackGain(0);
break;
case AudioManager.AUDIOFOCUS_LOSS:
// Permanent loss of focus probably due to another audio app, abandon focus
// and stop playback.
abandonAudioFocus();
sendAvrcpPause();
break;
}
break;
case DELAYED_PAUSE:
if (BluetoothMediaBrowserService.getPlaybackState()
== PlaybackStateCompat.STATE_PLAYING && !inCallFromStreamingDevice()) {
sendAvrcpPause();
mSentPause = true;
mStreamAvailable = false;
}
break;
default:
Log.w(TAG, "Received unexpected event: " + message.what);
}
}
/**
* Utility functions.
*/
private void requestAudioFocusIfNone() {
if (DBG) Log.d(TAG, "requestAudioFocusIfNone()");
if (mAudioFocus != AudioManager.AUDIOFOCUS_GAIN) {
requestAudioFocus();
}
}
private synchronized int requestAudioFocus() {
if (DBG) Log.d(TAG, "requestAudioFocus()");
// Bluetooth A2DP may carry Music, Audio Books, Navigation, or other sounds so mark content
// type unknown.
AudioAttributes streamAttributes =
new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
.build();
// Bluetooth ducking is handled at the native layer at the request of AudioManager.
AudioFocusRequest focusRequest =
new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).setAudioAttributes(
streamAttributes)
.setOnAudioFocusChangeListener(mAudioFocusListener, this)
.build();
int focusRequestStatus = mAudioManager.requestAudioFocus(focusRequest);
// If the request is granted begin streaming immediately and schedule an upgrade.
if (focusRequestStatus == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
startFluorideStreaming();
mAudioFocus = AudioManager.AUDIOFOCUS_GAIN;
}
return focusRequestStatus;
}
/**
* Plays a silent audio sample so that MediaSessionService will be aware of the fact that
* Bluetooth is playing audio.
*
* Creates a new MediaPlayer if one does not already exist. Repeat calls to this function are
* safe and will result in the silent audio sample again.
*
* This allows the MediaSession in AVRCP Controller to be routed media key events, if we've
* chosen to use it.
*/
private synchronized void requestMediaKeyFocus() {
if (DBG) Log.d(TAG, "requestMediaKeyFocus()");
if (mMediaPlayer == null) {
AudioAttributes attrs = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.build();
mMediaPlayer = MediaPlayer.create(mContext, R.raw.silent, attrs,
mAudioManager.generateAudioSessionId());
if (mMediaPlayer == null) {
Log.e(TAG, "Failed to initialize media player. You may not get media key events");
return;
}
mMediaPlayer.setLooping(false);
mMediaPlayer.setOnErrorListener((mp, what, extra) -> {
Log.e(TAG, "Silent media player error: " + what + ", " + extra);
releaseMediaKeyFocus();
return false;
});
}
mMediaPlayer.start();
BluetoothMediaBrowserService.setActive(true);
}
private synchronized void abandonAudioFocus() {
if (DBG) Log.d(TAG, "abandonAudioFocus()");
stopFluorideStreaming();
mAudioManager.abandonAudioFocus(mAudioFocusListener);
mAudioFocus = AudioManager.AUDIOFOCUS_NONE;
}
/**
* Destroys the silent audio sample MediaPlayer, notifying MediaSessionService of the fact
* we're no longer playing audio.
*/
private synchronized void releaseMediaKeyFocus() {
if (DBG) Log.d(TAG, "releaseMediaKeyFocus()");
if (mMediaPlayer == null) {
return;
}
BluetoothMediaBrowserService.setActive(false);
mMediaPlayer.stop();
mMediaPlayer.release();
mMediaPlayer = null;
}
private void startFluorideStreaming() {
mA2dpSinkService.informAudioFocusStateNative(STATE_FOCUS_GRANTED);
mA2dpSinkService.informAudioTrackGainNative(1.0f);
requestMediaKeyFocus();
}
private void stopFluorideStreaming() {
releaseMediaKeyFocus();
mA2dpSinkService.informAudioFocusStateNative(STATE_FOCUS_LOST);
}
private void setFluorideAudioTrackGain(float gain) {
mA2dpSinkService.informAudioTrackGainNative(gain);
}
private void sendAvrcpPause() {
BluetoothMediaBrowserService.pause();
}
private void sendAvrcpPlay() {
BluetoothMediaBrowserService.play();
}
private boolean inCallFromStreamingDevice() {
BluetoothDevice targetDevice = null;
List<BluetoothDevice> connectedDevices = mA2dpSinkService.getConnectedDevices();
if (!connectedDevices.isEmpty()) {
targetDevice = connectedDevices.get(0);
}
HeadsetClientService headsetClientService = HeadsetClientService.getHeadsetClientService();
if (targetDevice != null && headsetClientService != null) {
List<BluetoothHeadsetClientCall> currentCalls =
headsetClientService.getCurrentCalls(targetDevice);
if (currentCalls == null) return false;
return currentCalls.size() > 0;
}
return false;
}
synchronized int getAudioFocus() {
return mAudioFocus;
}
private boolean isIotDevice() {
return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_EMBEDDED);
}
private boolean isTvDevice() {
return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
}
private boolean shouldRequestFocus() {
return mContext.getResources()
.getBoolean(R.bool.a2dp_sink_automatically_request_audio_focus);
}
}