/* * 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); } /** * Safely clean up this stream handler object */ public void cleanup() { abandonAudioFocus(); removeCallbacksAndMessages(null); } 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 connectedDevices = mA2dpSinkService.getConnectedDevices(); if (!connectedDevices.isEmpty()) { targetDevice = connectedDevices.get(0); } HeadsetClientService headsetClientService = HeadsetClientService.getHeadsetClientService(); if (targetDevice != null && headsetClientService != null) { List 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); } }