| /* |
| * 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.content.Context; |
| import android.media.AudioAttributes; |
| import android.media.AudioManager; |
| import android.media.AudioManager.OnAudioFocusChangeListener; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.util.Log; |
| |
| import com.android.bluetooth.avrcpcontroller.AvrcpControllerService; |
| import com.android.bluetooth.R; |
| |
| /** |
| * 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 boolean DBG = false; |
| private static final String TAG = "A2dpSinkStreamHandler"; |
| |
| // Configuration Variables |
| private static final int DEFAULT_DUCK_PERCENT = 25; |
| |
| // 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 |
| |
| // 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 A2dpSinkStateMachine mA2dpSinkSm; |
| 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; |
| |
| // Focus changes when we are currently holding focus. |
| private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() { |
| public void onAudioFocusChange(int focusChange) { |
| if (DBG) { |
| Log.d(TAG, "onAudioFocusChangeListener focuschange " + focusChange); |
| } |
| A2dpSinkStreamHandler.this.obtainMessage(AUDIO_FOCUS_CHANGE, focusChange) |
| .sendToTarget(); |
| } |
| }; |
| |
| public A2dpSinkStreamHandler(A2dpSinkStateMachine a2dpSinkSm, Context context) { |
| mA2dpSinkSm = a2dpSinkSm; |
| mContext = context; |
| mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); |
| } |
| |
| @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: |
| // Audio stream has started, stop it if we don't have focus. |
| mStreamAvailable = true; |
| if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) { |
| sendAvrcpPause(); |
| } else { |
| startAvrcpUpdates(); |
| } |
| break; |
| |
| case SRC_STR_STOP: |
| // Audio stream has stopped, maintain focus but stop avrcp updates. |
| mStreamAvailable = false; |
| stopAvrcpUpdates(); |
| break; |
| |
| case SNK_PLAY: |
| // Local play command, gain focus and start avrcp updates. |
| if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) { |
| requestAudioFocus(); |
| } |
| startAvrcpUpdates(); |
| break; |
| |
| case SNK_PAUSE: |
| // Local pause command, maintain focus but stop avrcp updates. |
| stopAvrcpUpdates(); |
| break; |
| |
| case SRC_PLAY: |
| // Remote play command, if we have audio focus update avrcp, otherwise send pause. |
| if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) { |
| sendAvrcpPause(); |
| } else { |
| startAvrcpUpdates(); |
| } |
| break; |
| |
| case SRC_PAUSE: |
| // Remote pause command, stop avrcp updates. |
| stopAvrcpUpdates(); |
| break; |
| |
| case DISCONNECT: |
| // Remote device has disconnected, restore everything to default state. |
| sendAvrcpPause(); |
| stopAvrcpUpdates(); |
| abandonAudioFocus(); |
| mSentPause = false; |
| break; |
| |
| case AUDIO_FOCUS_CHANGE: |
| // message.obj is the newly granted audio focus. |
| switch ((int) message.obj) { |
| case AudioManager.AUDIOFOCUS_GAIN: |
| // Begin playing audio, if we paused the remote, send a play now. |
| startAvrcpUpdates(); |
| 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. |
| if (mStreamAvailable) { |
| sendAvrcpPause(); |
| mSentPause = true; |
| } |
| stopFluorideStreaming(); |
| break; |
| |
| case AudioManager.AUDIOFOCUS_LOSS: |
| // Permanent loss of focus probably due to another audio app, abandon focus |
| // and stop playback. |
| mAudioFocus = AudioManager.AUDIOFOCUS_NONE; |
| abandonAudioFocus(); |
| sendAvrcpPause(); |
| break; |
| } |
| break; |
| |
| default: |
| Log.w(TAG, "Received unexpected event: " + message.what); |
| } |
| } |
| |
| /** |
| * Utility functions. |
| */ |
| private int requestAudioFocus() { |
| int focusRequestStatus = mAudioManager.requestAudioFocus( |
| mAudioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); |
| // If the request is granted begin streaming immediately and schedule an upgrade. |
| if (focusRequestStatus == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { |
| startAvrcpUpdates(); |
| startFluorideStreaming(); |
| mAudioFocus = AudioManager.AUDIOFOCUS_GAIN; |
| } |
| return focusRequestStatus; |
| } |
| |
| |
| private void abandonAudioFocus() { |
| stopFluorideStreaming(); |
| mAudioManager.abandonAudioFocus(mAudioFocusListener); |
| mAudioFocus = AudioManager.AUDIOFOCUS_NONE; |
| } |
| |
| private void startFluorideStreaming() { |
| mA2dpSinkSm.informAudioFocusStateNative(STATE_FOCUS_GRANTED); |
| mA2dpSinkSm.informAudioTrackGainNative(1.0f); |
| } |
| |
| private void stopFluorideStreaming() { |
| mA2dpSinkSm.informAudioFocusStateNative(STATE_FOCUS_LOST); |
| } |
| |
| private void setFluorideAudioTrackGain(float gain) { |
| mA2dpSinkSm.informAudioTrackGainNative(gain); |
| } |
| |
| private void startAvrcpUpdates() { |
| // Since AVRCP gets started after A2DP we may need to request it later in cycle. |
| AvrcpControllerService avrcpService = AvrcpControllerService.getAvrcpControllerService(); |
| |
| if (DBG) { |
| Log.d(TAG, "startAvrcpUpdates"); |
| } |
| if (avrcpService != null && avrcpService.getConnectedDevices().size() == 1) { |
| avrcpService.startAvrcpUpdates(); |
| } else { |
| Log.e(TAG, "startAvrcpUpdates failed because of connection."); |
| } |
| } |
| |
| private void stopAvrcpUpdates() { |
| // Since AVRCP gets started after A2DP we may need to request it later in cycle. |
| AvrcpControllerService avrcpService = AvrcpControllerService.getAvrcpControllerService(); |
| |
| if (DBG) { |
| Log.d(TAG, "stopAvrcpUpdates"); |
| } |
| if (avrcpService != null && avrcpService.getConnectedDevices().size() == 1) { |
| avrcpService.stopAvrcpUpdates(); |
| } else { |
| Log.e(TAG, "stopAvrcpUpdates failed because of connection."); |
| } |
| } |
| |
| private void sendAvrcpPause() { |
| // Since AVRCP gets started after A2DP we may need to request it later in cycle. |
| AvrcpControllerService avrcpService = AvrcpControllerService.getAvrcpControllerService(); |
| |
| if (DBG) { |
| Log.d(TAG, "sendAvrcpPause"); |
| } |
| if (avrcpService != null && avrcpService.getConnectedDevices().size() == 1) { |
| if (DBG) { |
| Log.d(TAG, "Pausing AVRCP."); |
| } |
| avrcpService.sendPassThroughCmd(avrcpService.getConnectedDevices().get(0), |
| AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE, |
| AvrcpControllerService.KEY_STATE_PRESSED); |
| avrcpService.sendPassThroughCmd(avrcpService.getConnectedDevices().get(0), |
| AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE, |
| AvrcpControllerService.KEY_STATE_RELEASED); |
| } else { |
| Log.e(TAG, "Passthrough not sent, connection un-available."); |
| } |
| } |
| |
| private void sendAvrcpPlay() { |
| // Since AVRCP gets started after A2DP we may need to request it later in cycle. |
| AvrcpControllerService avrcpService = AvrcpControllerService.getAvrcpControllerService(); |
| |
| if (DBG) { |
| Log.d(TAG, "sendAvrcpPlay"); |
| } |
| if (avrcpService != null && avrcpService.getConnectedDevices().size() == 1) { |
| if (DBG) { |
| Log.d(TAG, "Playing AVRCP."); |
| } |
| avrcpService.sendPassThroughCmd(avrcpService.getConnectedDevices().get(0), |
| AvrcpControllerService.PASS_THRU_CMD_ID_PLAY, |
| AvrcpControllerService.KEY_STATE_PRESSED); |
| avrcpService.sendPassThroughCmd(avrcpService.getConnectedDevices().get(0), |
| AvrcpControllerService.PASS_THRU_CMD_ID_PLAY, |
| AvrcpControllerService.KEY_STATE_RELEASED); |
| } else { |
| Log.e(TAG, "Passthrough not sent, connection un-available."); |
| } |
| } |
| } |