Play a short/silent WAV file to ensure we get routed media key events

Bug: b/112431289
Test: Pair BT phone and play music. Use adb shell input keyevent
to send all of 126/play, 127/pause, 86/stop, 88/prev, 87/next,
164/mute, 25/volume-down, 24/volume-up. Stream and functions behave as
expected. Additionally, check that dumpsys media_session shows bluetooth
as both the MediaButtonSession AND a valid audio playback source.

Change-Id: Ic9d096f4b372f3fd45448d01d953e8d38e7b2278
Merged-In: Ic9d096f4b372f3fd45448d01d953e8d38e7b2278
diff --git a/res/raw/silent.wav b/res/raw/silent.wav
new file mode 100755
index 0000000..d6d93ef
--- /dev/null
+++ b/res/raw/silent.wav
Binary files differ
diff --git a/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java b/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java
index 323dbfb..4dfb8d4 100644
--- a/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java
+++ b/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java
@@ -23,7 +23,10 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
 import android.media.MediaMetadata;
+import android.media.MediaPlayer;
 import android.media.browse.MediaBrowser;
 import android.media.browse.MediaBrowser.MediaItem;
 import android.media.session.MediaController;
@@ -95,8 +98,16 @@
             "com.android.bluetooth.avrcpcontroller.CUSTOM_ACTION_GET_PLAY_STATUS_NATIVE";
 
     private static BluetoothMediaBrowserService sBluetoothMediaBrowserService;
+
+    // In order 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 us media events. This is a restriction currently imposed by the media
+    // framework code and could be reconsidered in the future.
     private MediaSession mSession;
     private MediaMetadata mA2dpMetadata;
+    private MediaPlayer mMediaPlayer;
 
     private AvrcpControllerService mAvrcpCtrlSrvc;
     private boolean mBrowseConnected = false;
@@ -158,23 +169,37 @@
         }
     }
 
+    /**
+     * Initialize this BluetoothMediaBrowserService, creating our MediaSession, MediaPlayer and
+     * MediaMetaData, and setting up mechanisms to talk with the AvrcpControllerService.
+     */
     @Override
     public void onCreate() {
         if (DBG) Log.d(TAG, "onCreate");
         super.onCreate();
 
+        // Create and configure the MediaSession
         mSession = new MediaSession(this, TAG);
-        setSessionToken(mSession.getSessionToken());
         mSession.setCallback(mSessionCallbacks);
         mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS
                 | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
-        mSession.setActive(true);
         mSession.setQueueTitle(getString(R.string.bluetooth_a2dp_sink_queue_name));
         mSession.setQueue(mMediaQueue);
+
+        // Create and setup the MediaPlayer
+        initMediaPlayer();
+
+        // Associate the held MediaSession with this browser and activate it
+        setSessionToken(mSession.getSessionToken());
+        mSession.setActive(true);
+
+        // Internal handler to process events and requests
         mAvrcpCommandQueue = new AvrcpCommandQueueHandler(Looper.getMainLooper(), this);
 
+        // Set the initial Media state (sets current playback state and media meta data)
         refreshInitialPlayingState();
 
+        // Set up communication with the controller service
         IntentFilter filter = new IntentFilter();
         filter.addAction(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
         filter.addAction(AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED);
@@ -185,19 +210,82 @@
         synchronized (this) {
             mParentIdToRequestMap.clear();
         }
-        setBluetoothMediaBrowserService(this);
 
+        setBluetoothMediaBrowserService(this);
     }
 
+    /**
+     * Clean up this instance in the reverse order that we created it.
+     */
     @Override
     public void onDestroy() {
         if (DBG) Log.d(TAG, "onDestroy");
         setBluetoothMediaBrowserService(null);
-        mSession.release();
         unregisterReceiver(mBtReceiver);
+        destroyMediaPlayer();
+        mSession.release();
         super.onDestroy();
     }
 
+    /**
+     * Initializes the silent MediaPlayer object which aids in receiving media key focus.
+     *
+     * The created MediaPlayer is already prepared and will release and stop itself on error. All
+     * you need to do is start() it.
+     */
+    private void initMediaPlayer() {
+        if (DBG) Log.d(TAG, "initMediaPlayer()");
+
+        // Parameters for create
+        AudioAttributes attrs = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_MEDIA)
+                .build();
+        AudioManager am = getSystemService(AudioManager.class);
+
+        // Create our player object. Returns a prepared player on success, null on failure
+        mMediaPlayer = MediaPlayer.create(this, R.raw.silent, attrs, am.generateAudioSessionId());
+        if (mMediaPlayer == null) {
+            Log.e(TAG, "Failed to initialize media player. You may not get media key events");
+            return;
+        }
+
+        // Set other player attributes
+        mMediaPlayer.setLooping(false);
+        mMediaPlayer.setOnErrorListener((mp, what, extra) -> {
+            Log.e(TAG, "Silent media player error: " + what + ", " + extra);
+            destroyMediaPlayer();
+            return false;
+        });
+    }
+
+    /**
+     * Safely tears down our local MediaPlayer
+     */
+    private void destroyMediaPlayer() {
+        if (DBG) Log.d(TAG, "destroyMediaPlayer()");
+        if (mMediaPlayer == null) {
+            return;
+        }
+        mMediaPlayer.stop();
+        mMediaPlayer.release();
+        mMediaPlayer = null;
+    }
+
+    /**
+     * Uses the internal MediaPlayer to play a silent, short audio sample so that AudioService will
+     * treat us as the active MediaSession/MediaPlayer combo and properly route us media key events.
+     *
+     * If the MediaPlayer failed to initialize properly, this call will fail gracefully and log the
+     * failed attempt. Media keys will not be routed.
+     */
+    private void getMediaKeyFocus() {
+        if (DBG) Log.d(TAG, "getMediaKeyFocus()");
+        if (mMediaPlayer == null) {
+            Log.w(TAG, "Media player is null. Can't get media key focus. Media keys may not route");
+            return;
+        }
+        mMediaPlayer.start();
+    }
 
     /**
      *  getBluetoothMediaBrowserService()
@@ -311,6 +399,7 @@
             if (DBG) Log.d(TAG, "onPrepare");
             if (mA2dpSinkService != null) {
                 mA2dpSinkService.requestAudioFocus(mA2dpDevice, true);
+                getMediaKeyFocus();
             }
         }
 
@@ -336,6 +425,7 @@
                 // Play the item if possible.
                 if (mA2dpSinkService != null) {
                     mA2dpSinkService.requestAudioFocus(mA2dpDevice, true);
+                    getMediaKeyFocus();
                 }
                 mAvrcpCtrlSrvc.fetchAttrAndPlayItem(mA2dpDevice, mediaId);
             }