Basic framework support to test Avrcp/A2dp profiles.

Background and a design doc for the framework can be found in b/29097732

Add a MediaBrowserService to the test app for intercepting Media Keys.
Add a BluetoothMediaFacade for bluetooth media related test cases
Added an API to ConnectionFacade to disconnect on a specific list of profiles. Also re-factored some code here to share code.

Bug:  29097732, 28913426, 29100401
Change-Id: I1d172a1de342f19545f0a46f025effa7daf5ad4a
(cherry picked from commit 901bd7709544583e9497dc970105093180a5debf)
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothAvrcpMediaBrowserService.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothAvrcpMediaBrowserService.java
new file mode 100644
index 0000000..dfdc4c7
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothAvrcpMediaBrowserService.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * 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.googlecode.android_scripting.facade.bluetooth;
+
+import android.app.Service;
+import android.media.browse.MediaBrowser.MediaItem;
+import android.media.session.*;
+import android.os.Bundle;
+import android.service.media.MediaBrowserService;
+
+import com.googlecode.android_scripting.facade.bluetooth.BluetoothMediaFacade;
+import com.googlecode.android_scripting.Log;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A {@link MediaBrowserService} implemented in the SL4A App to intercept Media keys and
+ * commands.
+ * This would be running on the AVRCP TG device and whenever the device receives a media
+ * command from a AVRCP CT, this MediaBrowserService's MediaSession would intercept it.
+ * Helps to verify the commands received by the AVRCP TG are the same as what was sent from
+ * an AVRCP CT.
+ */
+public class BluetoothAvrcpMediaBrowserService extends MediaBrowserService {
+    private static final String TAG = "BluetoothAvrcpMBS";
+    private static final String MEDIA_ROOT_ID = "__ROOT__";
+
+    private MediaSession mMediaSession = null;
+    private MediaSession.Token mSessionToken = null;
+    private MediaController mMediaController = null;
+
+    /**
+     * MediaSession callback dispatching the corresponding <code>PlaybackState</code> to
+     * {@link BluetoothMediaFacade}
+     */
+    private MediaSession.Callback mMediaSessionCallback =
+            new MediaSession.Callback() {
+                @Override
+                public void onPlay() {
+                    Log.d(TAG + " onPlay");
+                    BluetoothMediaFacade.dispatchPlaybackStateChanged(PlaybackState.STATE_PLAYING);
+                }
+
+                @Override
+                public void onPause() {
+                    Log.d(TAG + " onPause");
+                    BluetoothMediaFacade.dispatchPlaybackStateChanged(PlaybackState.STATE_PAUSED);
+                }
+
+                @Override
+                public void onRewind() {
+                    Log.d(TAG + " onRewind");
+                    BluetoothMediaFacade.dispatchPlaybackStateChanged(PlaybackState.STATE_REWINDING);
+                }
+
+                @Override
+                public void onFastForward() {
+                    Log.d(TAG + " onFastForward");
+                    BluetoothMediaFacade.dispatchPlaybackStateChanged(PlaybackState.STATE_FAST_FORWARDING);
+                }
+
+                @Override
+                public void onSkipToNext() {
+                    Log.d(TAG + " onSkipToNext");
+                    BluetoothMediaFacade.dispatchPlaybackStateChanged(PlaybackState.STATE_SKIPPING_TO_NEXT);
+                }
+
+                @Override
+                public void onSkipToPrevious() {
+                    Log.d(TAG + " onSkipToPrevious");
+                    BluetoothMediaFacade.dispatchPlaybackStateChanged(
+                            PlaybackState.STATE_SKIPPING_TO_PREVIOUS);
+                }
+            };
+
+    /**
+     * We do the following on the AvrcpMediaBrowserService onCreate():
+     * 1. Create a new MediaSession
+     * 2. Register a callback with the created MediaSession
+     * 3. Set its playback state and set the session to active.
+     */
+    @Override
+    public void onCreate() {
+        Log.d(TAG + " onCreate");
+        super.onCreate();
+        mMediaSession = new MediaSession(this, TAG);
+        mSessionToken = mMediaSession.getSessionToken();
+        setSessionToken(mSessionToken);
+        mMediaSession.setCallback(mMediaSessionCallback);
+        mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS
+                | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
+        // Note - MediaButton Intent is not received until the session has a PlaybackState
+        // whose state is set to something other than STATE_STOPPED or STATE_NONE
+        PlaybackState state = new PlaybackState.Builder()
+                .setActions(PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PAUSE
+                        | PlaybackState.ACTION_FAST_FORWARD | PlaybackState.ACTION_PLAY_PAUSE
+                        | PlaybackState.ACTION_REWIND | PlaybackState.ACTION_SKIP_TO_NEXT
+                        | PlaybackState.ACTION_SKIP_TO_PREVIOUS)
+                .setState(PlaybackState.STATE_PLAYING, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 1)
+                .build();
+        mMediaSession.setPlaybackState(state);
+        mMediaSession.setActive(true);
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.d(TAG + " onDestroy");
+        mMediaSession.release();
+        mMediaSession = null;
+        mSessionToken = null;
+        super.onDestroy();
+    }
+
+    @Override
+    public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
+        BrowserRoot mediaRoot = new BrowserRoot(MEDIA_ROOT_ID, null);
+        return mediaRoot;
+    }
+
+    @Override
+    public void onLoadChildren(String parentId, Result<List<MediaItem>> result) {
+        List<MediaItem> mediaList = new ArrayList<MediaItem>();
+        result.sendResult(mediaList);
+    }
+
+    /**
+     * Returns the TAG string
+     * @return  <code>BluetoothAvrcpMediaBrowserService</code>'s tag
+     */
+    public static String getTag() {
+        return TAG;
+    }
+}
+
+
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothConnectionFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothConnectionFacade.java
index bcc5e38..e41d6c8 100644
--- a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothConnectionFacade.java
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothConnectionFacade.java
@@ -50,6 +50,9 @@
 import com.googlecode.android_scripting.rpc.Rpc;
 import com.googlecode.android_scripting.rpc.RpcParameter;
 
+import org.json.JSONArray;
+import org.json.JSONException;
+
 public class BluetoothConnectionFacade extends RpcReceiver {
 
     private final Service mService;
@@ -151,7 +154,7 @@
          * Constructor
          *
          * @param deviceID Either the device alias name or mac address.
-         * @param bond If true, bond the device only.
+         * @param bond     If true, bond the device only.
          */
         public DiscoverConnectReceiver(String deviceID) {
             super();
@@ -169,7 +172,7 @@
                     mBluetoothAdapter.cancelDiscovery();
                     mDevice = device;
                 }
-            // After discovery stops.
+                // After discovery stops.
             } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
                 if (mDevice == null) {
                     Log.d("Device " + mDeviceID + " not discovered.");
@@ -218,7 +221,7 @@
                     mBluetoothAdapter.cancelDiscovery();
                     mDevice = device;
                 }
-            // After discovery stops.
+                // After discovery stops.
             } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
                 if (mDevice == null) {
                     Log.d("Device " + mDeviceID + " was not discovered.");
@@ -271,37 +274,50 @@
                 Log.e("Action devices does match act: " + device + " exp " + mDeviceID);
                 return;
             }
+            // Find the state.
+            int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+            if (state == -1) {
+                Log.e("Action does not have a state.");
+                return;
+            }
+
+            String connState = "";
+            if (state == BluetoothProfile.STATE_CONNECTED) {
+                connState = "Connecting";
+            } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
+                connState = "Disconnecting";
+            }
 
             int profile = -1;
             String listener = "";
             switch (action) {
                 case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED:
                     profile = BluetoothProfile.A2DP;
-                    listener = "A2dpConnecting" + mDeviceID;
+                    listener = "A2dp" + connState + mDeviceID;
                     break;
                 case BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED:
                     profile = BluetoothProfile.INPUT_DEVICE;
-                    listener = "HidConnecting" + mDeviceID;
+                    listener = "Hid" + connState + mDeviceID;
                     break;
                 case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED:
                     profile = BluetoothProfile.HEADSET;
-                    listener = "HspConnecting" + mDeviceID;
+                    listener = "Hsp" + connState + mDeviceID;
                     break;
                 case BluetoothPan.ACTION_CONNECTION_STATE_CHANGED:
                     profile = BluetoothProfile.PAN;
-                    listener = "PanConnecting" + mDeviceID;
+                    listener = "Pan" + connState + mDeviceID;
                     break;
                 case BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED:
                     profile = BluetoothProfile.HEADSET_CLIENT;
-                    listener = "HfpClientConnecting" + mDeviceID;
+                    listener = "HfpClient" + connState + mDeviceID;
                     break;
                 case BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED:
                     profile = BluetoothProfile.A2DP_SINK;
-                    listener = "A2dpSinkConnecting" + mDeviceID;
+                    listener = "A2dpSink" + connState + mDeviceID;
                     break;
                 case BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED:
                     profile = BluetoothProfile.PBAP_CLIENT;
-                    listener = "PbapClientConnecting" + mDeviceID;
+                    listener = "PbapClient" + connState + mDeviceID;
                     break;
             }
 
@@ -310,15 +326,10 @@
                 return;
             }
 
-            // Find the state.
-            int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
-            if (state == -1) {
-                Log.e("Action does not have a state.");
-                return;
-            }
 
             // Post an event to Facade.
-            if (state == BluetoothProfile.STATE_CONNECTED) {
+            if ((state == BluetoothProfile.STATE_CONNECTED) || (state
+                    == BluetoothProfile.STATE_DISCONNECTED)) {
                 Bundle news = new Bundle();
                 news.putInt("profile", profile);
                 news.putInt("state", state);
@@ -329,6 +340,66 @@
         }
     }
 
+    /**
+     * Converts a given JSONArray to an ArrayList of Integers
+     *
+     * @param jsonArray the JSONArray to be converted
+     * @return <code>List<Integer></></code> the converted list of Integers
+     */
+    private List<Integer> jsonArrayToIntegerList(JSONArray jsonArray) throws JSONException {
+        if (jsonArray == null) {
+            return null;
+        }
+        List<Integer> intArray = new ArrayList<Integer>();
+        for (int i = 0; i < jsonArray.length(); i++) {
+            intArray.add(jsonArray.getInt(i));
+        }
+        return intArray;
+
+    }
+
+    /**
+     * Helper function used by{@link connectProfile} & {@link disconnectProfiles} to handle a
+     * successful return from a profile connect/disconnect call
+     *
+     * @param deviceID      The device id string
+     * @param profile       String representation of the profile - used to create keys in the
+     *                      <code>listeningDevices</code> hashMap.
+     * @param connState     String that denotes if this is called after a connect or disconnect
+     *                      call.  Helps to share code between connect & disconnect
+     * @param profileFilter The <code>IntentFilter</code> for this profile that we want to listen
+     *                      on.
+     */
+    private void handleConnectionStateChanged(String deviceID, String profile, String connState,
+            IntentFilter profileFilter) {
+        Log.d(connState + " " + profile);
+        ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
+        mService.registerReceiver(receiver, profileFilter);
+        listeningDevices.put(profile + connState + deviceID, receiver);
+    }
+
+    /**
+     * Helper function used by{@link connectProfile} & {@link disconnectProfiles} to handle a
+     * unsuccessful return from a profile connect/disconnect call
+     *
+     * @param profile   String representation of the profile - used to post an Event on the
+     *                  <code>mEventFacade</code>
+     * @param connState String that denotes if this is called after a connect or disconnect call.
+     *                  Helps to share code between connect & disconnect
+     */
+    private void handleConnectionStateChangeFailed(String profile, String connState) {
+        Log.d("Failed Starting " + profile + " " + connState);
+        Bundle badNews = (Bundle) mBadNews.clone();
+        badNews.putString("Type", profile);
+        mEventFacade.postEvent(connState, badNews);
+    }
+
+    /**
+     * Connect on all the profiles to the given Bluetooth device
+     *
+     * @param device   The <code>BluetoothDevice</code> to connect to
+     * @param deviceID Name (String) of the device to connect to
+     */
     private void connectProfile(BluetoothDevice device, String deviceID) {
         mService.registerReceiver(mPairingHelper, mPairingFilter);
         ParcelUuid[] deviceUuids = device.getUuids();
@@ -340,94 +411,72 @@
         if (BluetoothUuid.containsAnyUuid(BluetoothA2dpFacade.SINK_UUIDS, deviceUuids)) {
             boolean status = mA2dpProfile.a2dpConnect(device);
             if (status) {
-                Log.d("Connecting A2dp...");
-                ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
-                mService.registerReceiver(receiver, mA2dpStateChangeFilter);
-                listeningDevices.put("A2dpConnecting" + deviceID, receiver);
+                handleConnectionStateChanged(deviceID, "A2dp", "Connecting", mA2dpStateChangeFilter);
             } else {
-                Log.d("Failed starting A2dp connection.");
-                Bundle a2dpBadNews = (Bundle) mBadNews.clone();
-                a2dpBadNews.putString("Type", "a2dp");
-                mEventFacade.postEvent("Connect", a2dpBadNews);
+                handleConnectionStateChangeFailed("a2dp", "Connect");
             }
         }
         if (BluetoothUuid.containsAnyUuid(BluetoothA2dpSinkFacade.SOURCE_UUIDS, deviceUuids)) {
             boolean status = mA2dpSinkProfile.a2dpSinkConnect(device);
             if (status) {
-                Log.d("Connecting A2dp Sink...");
-                ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
-                mService.registerReceiver(receiver, mA2dpSinkStateChangeFilter);
-                listeningDevices.put("A2dpSinkConnecting" + deviceID, receiver);
+                handleConnectionStateChanged(deviceID, "A2dpSink", "Connecting",
+                        mA2dpSinkStateChangeFilter);
             } else {
-                Log.d("Failed starting A2dp Sink connection.");
-                Bundle a2dpSinkBadNews = (Bundle) mBadNews.clone();
-                a2dpSinkBadNews.putString("Type", "a2dpsink");
-                mEventFacade.postEvent("Connect", a2dpSinkBadNews);
+                handleConnectionStateChangeFailed("a2dpsink", "Connect");
             }
         }
         if (BluetoothUuid.containsAnyUuid(BluetoothHidFacade.UUIDS, deviceUuids)) {
             boolean status = mHidProfile.hidConnect(device);
             if (status) {
-                Log.d("Connecting Hid...");
-                ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
-                mService.registerReceiver(receiver, mHidStateChangeFilter);
-                listeningDevices.put("HidConnecting" + deviceID, receiver);
+                handleConnectionStateChanged(deviceID, "Hid", "Connecting", mHidStateChangeFilter);
             } else {
-                Log.d("Failed starting Hid connection.");
-                mEventFacade.postEvent("HidConnect" + deviceID, mBadNews);
+                handleConnectionStateChangeFailed("Hid", "Connect");
             }
         }
         if (BluetoothUuid.containsAnyUuid(BluetoothHspFacade.UUIDS, deviceUuids)) {
             boolean status = mHspProfile.hspConnect(device);
             if (status) {
-                Log.d("Connecting Hsp...");
-                ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
-                mService.registerReceiver(receiver, mHspStateChangeFilter);
-                listeningDevices.put("HspConnecting" + deviceID, receiver);
+                handleConnectionStateChanged(deviceID, "Hsp", "Connecting", mHspStateChangeFilter);
             } else {
-                Log.d("Failed starting Hsp connection.");
-                mEventFacade.postEvent("HspConnect" + deviceID, mBadNews);
+                handleConnectionStateChangeFailed("Hsp", "Connect");
             }
         }
         if (BluetoothUuid.containsAnyUuid(BluetoothHfpClientFacade.UUIDS, deviceUuids)) {
             boolean status = mHfpClientProfile.hfpClientConnect(device);
             if (status) {
-                Log.d("Connecting HFP Client ...");
-                ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
-                mService.registerReceiver(receiver, mHfpClientStateChangeFilter);
-                listeningDevices.put("HfpClientConnecting" + deviceID, receiver);
+                handleConnectionStateChanged(deviceID, "HfpClient", "Connecting",
+                        mHfpClientStateChangeFilter);
             } else {
-                Log.d("Failed starting Hfp Client connection.");
-                mEventFacade.postEvent("HfpClientConnect" + deviceID, mBadNews);
+                handleConnectionStateChangeFailed("HfpClient", "Connect");
             }
         }
         if (BluetoothUuid.containsAnyUuid(BluetoothPanFacade.UUIDS, deviceUuids)) {
             boolean status = mPanProfile.panConnect(device);
             if (status) {
-                Log.d("Connecting Pan...");
-                ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
-                mService.registerReceiver(receiver, mPanStateChangeFilter);
-                listeningDevices.put("PanConnecting" + deviceID, receiver);
+                handleConnectionStateChanged(deviceID, "Pan", "Connecting",
+                        mHfpClientStateChangeFilter);
             } else {
-                Log.d("Failed starting Pan connection.");
-                mEventFacade.postEvent("PanConnect" + deviceID, mBadNews);
+                handleConnectionStateChangeFailed("Pan", "Connect");
             }
         }
         if (BluetoothUuid.containsAnyUuid(BluetoothPbapClientFacade.UUIDS, deviceUuids)) {
             boolean status = mPbapClientProfile.pbapClientConnect(device);
             if (status) {
-                Log.d("Connecting PBAP Client ...");
-                ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
-                mService.registerReceiver(receiver, mPbapClientStateChangeFilter);
-                listeningDevices.put("PbapClientConnecting" + deviceID, receiver);
+                handleConnectionStateChanged(deviceID, "PbapClient", "Connecting",
+                        mPbapClientStateChangeFilter);
             } else {
-                Log.d("Failed starting Pbap Client connection.");
-                mEventFacade.postEvent("PbapClientConnect" + deviceID, mBadNews);
+                handleConnectionStateChangeFailed("PbapClient", "Connect");
             }
         }
         mService.unregisterReceiver(mPairingHelper);
     }
 
+    /**
+     * Disconnect on all available profiles from the given device
+     *
+     * @param device   The <code>BluetoothDevice</code> to disconnect from
+     * @param deviceID Name (String) of the device to disconnect from
+     */
     private void disconnectProfiles(BluetoothDevice device, String deviceID) {
         Log.d("Disconnecting device " + device);
         // Blindly disconnect all profiles. We may not have some of them connected so that will be a
@@ -441,6 +490,79 @@
         mPanProfile.panDisconnect(device);
     }
 
+    /**
+     * Disconnect from specific profiles provided in the given List of profiles.
+     *
+     * @param device     The {@link BluetoothDevice} to disconnect from
+     * @param deviceID   Name/BDADDR (String) of the device to disconnect from
+     * @param profileIds The list of profiles we want to disconnect on.
+     */
+    private void disconnectProfiles(BluetoothDevice device, String deviceID,
+            List<Integer> profileIds) {
+        boolean result;
+        for (int profileId : profileIds) {
+            switch (profileId) {
+                case BluetoothProfile.A2DP_SINK:
+                    result = mA2dpSinkProfile.a2dpSinkDisconnect(device);
+                    if (result) {
+                        handleConnectionStateChanged(deviceID, "A2dpSink", "Disconnecting",
+                                mA2dpSinkStateChangeFilter);
+                    } else {
+                        handleConnectionStateChangeFailed("a2dpsink", "Disconnect");
+                    }
+                    break;
+                case BluetoothProfile.A2DP:
+                    result = mA2dpProfile.a2dpDisconnect(device);
+                    if (result) {
+                        handleConnectionStateChanged(deviceID, "A2dp", "Disconnecting",
+                                mA2dpStateChangeFilter);
+                    } else {
+                        handleConnectionStateChangeFailed("a2dp", "Disconnect");
+                    }
+                    break;
+                case BluetoothProfile.INPUT_DEVICE:
+                    result = mHidProfile.hidDisconnect(device);
+                    if (result) {
+                        handleConnectionStateChanged(deviceID, "Hid", "Disconnecting",
+                                mHidStateChangeFilter);
+                    } else {
+                        handleConnectionStateChangeFailed("Hid", "Disconnect");
+                    }
+                    break;
+                case BluetoothProfile.HEADSET:
+                    result = mHspProfile.hspDisconnect(device);
+                    if (result) {
+                        handleConnectionStateChanged(deviceID, "Hsp", "Disconnecting",
+                                mHspStateChangeFilter);
+                    } else {
+                        handleConnectionStateChangeFailed("Hsp", "Disconnect");
+                    }
+                    break;
+                case BluetoothProfile.HEADSET_CLIENT:
+                    result = mHfpClientProfile.hfpClientDisconnect(device);
+                    if (result) {
+                        handleConnectionStateChanged(deviceID, "HfpClient", "Disconnecting",
+                                mHfpClientStateChangeFilter);
+                    } else {
+                        handleConnectionStateChangeFailed("HfpClient", "Disconnect");
+                    }
+                    break;
+                case BluetoothProfile.PBAP_CLIENT:
+                    result = mPbapClientProfile.pbapClientDisconnect(device);
+                    if (result) {
+                        handleConnectionStateChanged(deviceID, "PbapClient", "Disconnecting",
+                                mPbapClientStateChangeFilter);
+                    } else {
+                        handleConnectionStateChangeFailed("PbapClient", "Disconnect");
+                    }
+                    break;
+                default:
+                    Log.d("Unknown Profile Id to disconnect from. Quitting");
+                    return; // returns on the first unknown profile  it encounters.
+            }
+        }
+    }
+
     @Rpc(description = "Start intercepting all bluetooth connection pop-ups.")
     public void bluetoothStartPairingHelper() {
         mService.registerReceiver(mPairingHelper, mPairingFilter);
@@ -473,11 +595,11 @@
     }
 
     @Rpc(description = "Return list of connected bluetooth devices over a profile",
-         returns = "List of devices connected over the profile")
+            returns = "List of devices connected over the profile")
     public List<BluetoothDevice> bluetoothGetConnectedDevicesOnProfile(
             @RpcParameter(name = "profileId",
-                          description = "profileId same as BluetoothProfile")
-            Integer profileId) {
+                    description = "profileId same as BluetoothProfile")
+                    Integer profileId) {
         BluetoothProfile profile = null;
         switch (profileId) {
             case BluetoothProfile.A2DP_SINK:
@@ -493,11 +615,11 @@
     }
 
     @Rpc(description = "Connect to a specified device once it's discovered.",
-         returns = "Whether discovery started successfully.")
+            returns = "Whether discovery started successfully.")
     public Boolean bluetoothDiscoverAndConnect(
             @RpcParameter(name = "deviceID",
-                          description = "Name or MAC address of a bluetooth device.")
-            String deviceID) {
+                    description = "Name or MAC address of a bluetooth device.")
+                    String deviceID) {
         mBluetoothAdapter.cancelDiscovery();
         if (listeningDevices.containsKey(deviceID)) {
             Log.d("This device is already in the process of discovery and connecting.");
@@ -510,11 +632,11 @@
     }
 
     @Rpc(description = "Bond to a specified device once it's discovered.",
-         returns = "Whether discovery started successfully. ")
+            returns = "Whether discovery started successfully. ")
     public Boolean bluetoothDiscoverAndBond(
             @RpcParameter(name = "deviceID",
-                          description = "Name or MAC address of a bluetooth device.")
-            String deviceID) {
+                    description = "Name or MAC address of a bluetooth device.")
+                    String deviceID) {
         mBluetoothAdapter.cancelDiscovery();
         if (listeningDevices.containsKey(deviceID)) {
             Log.d("This device is already in the process of discovery and bonding.");
@@ -536,11 +658,11 @@
     }
 
     @Rpc(description = "Unbond a device.",
-         returns = "Whether the device was successfully unbonded.")
+            returns = "Whether the device was successfully unbonded.")
     public Boolean bluetoothUnbond(
             @RpcParameter(name = "deviceID",
-                          description = "Name or MAC address of a bluetooth device.")
-            String deviceID) throws Exception {
+                    description = "Name or MAC address of a bluetooth device.")
+                    String deviceID) throws Exception {
         BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
                 deviceID);
         return mDevice.removeBond();
@@ -549,43 +671,56 @@
     @Rpc(description = "Connect to a device that is already bonded.")
     public void bluetoothConnectBonded(
             @RpcParameter(name = "deviceID",
-                          description = "Name or MAC address of a bluetooth device.")
-            String deviceID) throws Exception {
+                    description = "Name or MAC address of a bluetooth device.")
+                    String deviceID) throws Exception {
         BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
                 deviceID);
         connectProfile(mDevice, deviceID);
     }
 
-    // TODO: Split the disconnect RPC by profiles as well for granular control over the ACL
     @Rpc(description = "Disconnect from a device that is already connected.")
     public void bluetoothDisconnectConnected(
             @RpcParameter(name = "deviceID",
-                          description = "Name or MAC address of a bluetooth device.")
-            String deviceID) throws Exception {
+                    description = "Name or MAC address of a bluetooth device.")
+                    String deviceID) throws Exception {
         BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
                 deviceID);
         disconnectProfiles(mDevice, deviceID);
     }
 
+    @Rpc(description = "Disconnect on a profile from a device that is already connected.")
+    public void bluetoothDisconnectConnectedProfile(
+            @RpcParameter(name = "deviceID",
+                    description = "Name or MAC address of a bluetooth device.")
+                    String deviceID,
+            @RpcParameter(name = "profileSet",
+                    description = "List of profiles to disconnect from.")
+                    JSONArray profileSet
+    ) throws Exception {
+        BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
+                deviceID);
+        disconnectProfiles(mDevice, deviceID, jsonArrayToIntegerList(profileSet));
+    }
+
     @Rpc(description = "Change permissions for a profile.")
     public void bluetoothChangeProfileAccessPermission(
             @RpcParameter(name = "deviceID",
-                          description = "Name or MAC address of a bluetooth device.")
-            String deviceID,
+                    description = "Name or MAC address of a bluetooth device.")
+                    String deviceID,
             @RpcParameter(name = "profileID",
-                          description = "Number of Profile to change access permission")
-            Integer profileID,
+                    description = "Number of Profile to change access permission")
+                    Integer profileID,
             @RpcParameter(name = "access",
-                          description = "Access level 0 = Unknown, 1 = Allowed, 2 = Rejected")
-            Integer access
-            ) throws Exception {
+                    description = "Access level 0 = Unknown, 1 = Allowed, 2 = Rejected")
+                    Integer access
+    ) throws Exception {
         if (access < 0 || access > 2) {
             Log.w("Unsupported access level.");
             return;
         }
         BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
                 deviceID);
-        switch(profileID) {
+        switch (profileID) {
             case BluetoothProfile.PBAP:
                 mDevice.setPhonebookAccessPermission(access);
                 break;
@@ -597,8 +732,12 @@
 
     @Override
     public void shutdown() {
-        for(BroadcastReceiver receiver : listeningDevices.values()) {
-            mService.unregisterReceiver(receiver);
+        for (BroadcastReceiver receiver : listeningDevices.values()) {
+            try {
+                mService.unregisterReceiver(receiver);
+            } catch (IllegalArgumentException ex) {
+                Log.e("Failed to unregister " + ex);
+            }
         }
         listeningDevices.clear();
         mService.unregisterReceiver(mPairingHelper);
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothMediaFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothMediaFacade.java
new file mode 100644
index 0000000..6c6aef4
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothMediaFacade.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * 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.googlecode.android_scripting.facade.bluetooth;
+
+import android.app.Service;
+import android.content.Intent;
+import android.content.ComponentName;
+import android.content.Context;
+
+import android.media.MediaMetadata;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.media.browse.MediaBrowser;
+import android.media.session.MediaController;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+import com.googlecode.android_scripting.Log;
+
+import java.util.List;
+
+/**
+ * SL4A Facade for dealing with Bluetooth Media related test cases
+ * The APIs here can be used on a AVRCP CT and TG, depending on what is tested.
+ */
+public class BluetoothMediaFacade extends RpcReceiver {
+    private static final String TAG = "BluetoothMediaFacade";
+    private static final boolean VDBG = false;
+    private final Service mService;
+    private final Context mContext;
+    private Handler mHandler;
+    private MediaSessionManager mSessionManager;
+    private MediaController mMediaController = null;
+    private MediaController.Callback mMediaCtrlCallback = null;
+    private MediaSessionManager.OnActiveSessionsChangedListener mSessionListener;
+    private MediaBrowser mBrowser = null;
+
+    private static EventFacade mEventFacade;
+    // Events posted
+    private static final String EVENT_PLAY_RECEIVED = "playReceived";
+    private static final String EVENT_PAUSE_RECEIVED = "pauseReceived";
+    private static final String EVENT_SKIP_PREV_RECEIVED = "skipPrevReceived";
+    private static final String EVENT_SKIP_NEXT_RECEIVED = "skipNextReceived";
+
+    // Commands received
+    private static final String CMD_MEDIA_PLAY = "play";
+    private static final String CMD_MEDIA_PAUSE = "pause";
+    private static final String CMD_MEDIA_SKIP_NEXT = "skipNext";
+    private static final String CMD_MEDIA_SKIP_PREV = "skipPrev";
+
+    private static final String bluetoothPkgName = "com.android.bluetooth";
+    private static final String browserServiceName =
+            "com.android.bluetooth.a2dpsink.mbs.A2dpMediaBrowserService";
+
+    public BluetoothMediaFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mEventFacade = manager.getReceiver(EventFacade.class);
+        mHandler = new Handler(Looper.getMainLooper());
+        mContext = mService.getApplicationContext();
+        mSessionManager =
+                (MediaSessionManager) mContext.getSystemService(mContext.MEDIA_SESSION_SERVICE);
+        mSessionListener = new SessionChangeListener();
+        // Listen on Active MediaSession changes, so we can get the active session's MediaController
+        if (mSessionManager != null) {
+            ComponentName compName =
+                    new ComponentName(mContext.getPackageName(), this.getClass().getName());
+            mSessionManager.addOnActiveSessionsChangedListener(mSessionListener, null,
+                    mHandler);
+            if (VDBG) {
+                List<MediaController> mcl = mSessionManager.getActiveSessions(null);
+                Log.d(TAG + " Num Sessions " + mcl.size());
+                for (int i = 0; i < mcl.size(); i++) {
+                    Log.d(TAG + "Active session : " + i + ((MediaController) (mcl.get(
+                            i))).getPackageName() + ((MediaController) (mcl.get(i))).getTag());
+                }
+            }
+        }
+        mMediaCtrlCallback = new MediaControllerCallback();
+    }
+
+    /**
+     * Class method called from {@link BluetoothAvrcpMediaBrowserService} to post an Event through
+     * EventFacade back to the RPC client.
+     *
+     * @param playbackState PlaybackState change that is posted as an Event to the client.
+     */
+    public static void dispatchPlaybackStateChanged(int playbackState) {
+        switch (playbackState) {
+            case PlaybackState.STATE_PLAYING:
+                mEventFacade.postEvent(EVENT_PLAY_RECEIVED, new Bundle());
+                break;
+            case PlaybackState.STATE_PAUSED:
+                mEventFacade.postEvent(EVENT_PAUSE_RECEIVED, new Bundle());
+                break;
+            case PlaybackState.STATE_SKIPPING_TO_NEXT:
+                mEventFacade.postEvent(EVENT_SKIP_NEXT_RECEIVED, new Bundle());
+                break;
+            case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
+                mEventFacade.postEvent(EVENT_SKIP_PREV_RECEIVED, new Bundle());
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * To register to MediaSession for callbacks on updates
+     */
+    private class MediaControllerCallback extends MediaController.Callback {
+        @Override
+        public void onPlaybackStateChanged(PlaybackState state) {
+            Log.d(TAG + " onPlaybackStateChanged: " + state.getState());
+        }
+
+        @Override
+        public void onMetadataChanged(MediaMetadata metadata) {
+            Log.d(TAG + " onMetadataChanged ");
+        }
+    }
+
+    /**
+     * To get the MediaController for the currently active MediaSession
+     */
+    private class SessionChangeListener
+            implements MediaSessionManager.OnActiveSessionsChangedListener {
+        @Override
+        public void onActiveSessionsChanged(List<MediaController> controllers) {
+            if (VDBG) {
+                Log.d(TAG + " onActiveSessionsChanged : " + controllers.size());
+                for (int i = 0; i < controllers.size(); i++) {
+                    Log.d(TAG + "Active session : " + i + ((MediaController) (controllers.get(
+                            i))).getPackageName() + ((MediaController) (controllers.get(
+                            i))).getTag());
+                }
+            }
+            // Whenever the list of ActiveSessions change, iterate through the list of active
+            // session and look for the one that belongs to the BluetoothAvrcpMediaBrowserService.
+            // If found, update our MediaController, we don't care for the other Active Media
+            // Sessions.
+            if (controllers.size() > 0) {
+                for (int i = 0; i < controllers.size(); i++) {
+                    MediaController controller = (MediaController) controllers.get(i);
+                    if (controller.getTag().contains(BluetoothAvrcpMediaBrowserService.getTag())) {
+                        setCurrentMediaController(controller);
+                        return;
+                    }
+                }
+                setCurrentMediaController(null);
+            } else {
+                setCurrentMediaController(null);
+            }
+        }
+    }
+
+    /**
+     * Callback on <code>MediaBrowser.connect()</code>
+     */
+    MediaBrowser.ConnectionCallback mBrowserConnectionCallback =
+            new MediaBrowser.ConnectionCallback() {
+                private static final String classTag = TAG + " BrowserConnectionCallback";
+
+                @Override
+                public void onConnected() {
+                    Log.d(classTag + " onConnected: session token " + mBrowser.getSessionToken());
+                    MediaController mediaController = new MediaController(mContext,
+                            mBrowser.getSessionToken());
+                    // Update the MediaController
+                    setCurrentMediaController(mediaController);
+                }
+
+                @Override
+                public void onConnectionFailed() {
+                    Log.d(classTag + " onConnectionFailed");
+                }
+            };
+
+    /**
+     * Update the MediaController.
+     * On the AVRCP CT side (Carkitt for ex), this MediaController
+     * would be the one associated with the
+     * com.android.bluetooth.a2dpsink.mbs.A2dpMediaBrowserService.
+     * On the AVRCP TG side (Phone for ex), this MediaController would
+     * be the one associated with the
+     * com.googlecode.android_scripting.facade.bluetooth.BluetoothAvrcpMediaBrowserService
+     */
+    private void setCurrentMediaController(MediaController controller) {
+        Handler mainHandler = new Handler(mContext.getMainLooper());
+        if (mMediaController == null && controller != null) {
+            Log.d(TAG + " Setting MediaController " + controller.getTag());
+            mMediaController = controller;
+            mMediaController.registerCallback(mMediaCtrlCallback);
+        } else if (mMediaController != null && controller != null) {
+            // We have a diff media controller
+            if (controller.getSessionToken().equals(mMediaController.getSessionToken())
+                    == false) {
+                Log.d(TAG + " Changing MediaController " + controller.getTag());
+                mMediaController.unregisterCallback(mMediaCtrlCallback);
+                mMediaController = controller;
+                mMediaController.registerCallback(mMediaCtrlCallback, mainHandler);
+            }
+        } else if (mMediaController != null && controller == null) {
+            // The new media controller is null probably because it doesn't support Transport
+            // Controls
+            Log.d(TAG + " Clearing MediaController " + mMediaController.getTag());
+            mMediaController.unregisterCallback(mMediaCtrlCallback);
+            mMediaController = controller;
+        }
+    }
+
+    // Sends the passthrough command through the currently active MediaController.
+    // If there isn't one, look for the currently active sessions and just pick the first one,
+    // just a fallback
+    @Rpc(description = "Simulate a passthrough command")
+    public void bluetoothMediaPassthrough(
+            @RpcParameter(name = "passthruCmd", description = "play/pause/skipFwd/skipBack")
+                    String passthruCmd) {
+        Log.d(TAG + "Passthrough Cmd " + passthruCmd);
+        if (mMediaController == null) {
+            Log.i(TAG + " Media Controller not ready - Grabbing existing one");
+            ComponentName name =
+                    new ComponentName(mContext.getPackageName(),
+                            mSessionListener.getClass().getName());
+            List<MediaController> listMC = mSessionManager.getActiveSessions(null);
+            if (listMC.size() > 0) {
+                if (VDBG) {
+                    Log.d(TAG + " Num Sessions " + listMC.size());
+                    for (int i = 0; i < listMC.size(); i++) {
+                        Log.d(TAG + "Active session : " + i + ((MediaController) (listMC.get(
+                                i))).getPackageName() + ((MediaController) (listMC.get(
+                                i))).getTag());
+                    }
+                }
+                mMediaController = (MediaController) listMC.get(0);
+            } else {
+                Log.d(TAG + " No Active Media Session to grab");
+                return;
+            }
+        }
+
+        switch (passthruCmd) {
+            case CMD_MEDIA_PLAY:
+                mMediaController.getTransportControls().play();
+                break;
+            case CMD_MEDIA_PAUSE:
+                mMediaController.getTransportControls().pause();
+                break;
+            case CMD_MEDIA_SKIP_NEXT:
+                mMediaController.getTransportControls().skipToNext();
+                break;
+            case CMD_MEDIA_SKIP_PREV:
+                mMediaController.getTransportControls().skipToPrevious();
+                break;
+            default:
+                Log.d(TAG + " Unsupported Passthrough Cmd");
+                break;
+        }
+    }
+
+    // This is usually called from the AVRCP CT device (Carkitt) to connect to the
+    // existing com.android.bluetooth.a2dpsink.mbs.A2dpMediaBrowserservice
+    @Rpc(description = "Connect a MediaBrowser to the A2dpMediaBrowserservice in the Carkitt")
+    public void bluetoothMediaConnectToA2dpMediaBrowserService() {
+        ComponentName compName;
+        // Create a MediaBrowser to connect to the A2dpMBS
+        if (mBrowser == null) {
+            compName = new ComponentName(bluetoothPkgName, browserServiceName);
+            // Note - MediaBrowser connect needs to be done on the Main Thread's handler,
+            // otherwise we never get the ServiceConnected callback.
+            Runnable createAndConnectMediaBrowser = new Runnable() {
+                @Override
+                public void run() {
+                    mBrowser = new MediaBrowser(mContext, compName, mBrowserConnectionCallback,
+                            null);
+                    if (mBrowser != null) {
+                        Log.d(TAG + " Connecting to MBS");
+                        mBrowser.connect();
+                    } else {
+                        Log.d(TAG + " Failed to create a MediaBrowser");
+                    }
+                }
+            };
+
+            Handler mainHandler = new Handler(mContext.getMainLooper());
+            mainHandler.post(createAndConnectMediaBrowser);
+        } //mBrowser
+    }
+
+    // This is usually called from the AVRCP TG device (Phone)
+    @Rpc(description = "Start the BluetoothAvrcpMediaBrowserService.")
+    public void bluetoothMediaAvrcpMediaBrowserServiceStart() {
+        Log.d(TAG + "Staring BluetoothAvrcpMediaBrowserService");
+        // Start the Avrcp Media Browser service.  Starting it sets it to active.
+        Intent startIntent = new Intent(mContext, BluetoothAvrcpMediaBrowserService.class);
+        mContext.startService(startIntent);
+    }
+
+    @Rpc(description = "Stop the BluetoothAvrcpMediaBrowserService.")
+    public void bluetoothMediaAvrcpMediaBrowserServiceStop() {
+        Log.d(TAG + "Stopping BluetoothAvrcpMediaBrowserService");
+        // Stop the Avrcp Media Browser service.
+        Intent stopIntent = new Intent(mContext, BluetoothAvrcpMediaBrowserService.class);
+        mContext.stopService(stopIntent);
+    }
+
+    @Override
+    public void shutdown() {
+        setCurrentMediaController(null);
+    }
+}
diff --git a/ScriptingLayer/src/com/googlecode/android_scripting/facade/FacadeConfiguration.java b/ScriptingLayer/src/com/googlecode/android_scripting/facade/FacadeConfiguration.java
index 866ab6b..a42ff81 100644
--- a/ScriptingLayer/src/com/googlecode/android_scripting/facade/FacadeConfiguration.java
+++ b/ScriptingLayer/src/com/googlecode/android_scripting/facade/FacadeConfiguration.java
@@ -39,6 +39,7 @@
 import com.googlecode.android_scripting.facade.bluetooth.BluetoothLeAdvertiseFacade;
 import com.googlecode.android_scripting.facade.bluetooth.BluetoothLeScanFacade;
 import com.googlecode.android_scripting.facade.bluetooth.BluetoothMapFacade;
+import com.googlecode.android_scripting.facade.bluetooth.BluetoothMediaFacade;
 import com.googlecode.android_scripting.facade.bluetooth.BluetoothPbapClientFacade;
 import com.googlecode.android_scripting.facade.bluetooth.BluetoothPanFacade;
 import com.googlecode.android_scripting.facade.bluetooth.BluetoothRfcommFacade;
@@ -122,6 +123,7 @@
         sFacadeClassList.add(BluetoothHidFacade.class);
         sFacadeClassList.add(BluetoothMapFacade.class);
         sFacadeClassList.add(BluetoothPanFacade.class);
+        sFacadeClassList.add(BluetoothMediaFacade.class);
         sFacadeClassList.add(BluetoothRfcommFacade.class);
         sFacadeClassList.add(WebCamFacade.class);
         sFacadeClassList.add(WifiP2pManagerFacade.class);
diff --git a/ScriptingLayerForAndroid/AndroidManifest.xml b/ScriptingLayerForAndroid/AndroidManifest.xml
index bce4fc7..eef182f 100644
--- a/ScriptingLayerForAndroid/AndroidManifest.xml
+++ b/ScriptingLayerForAndroid/AndroidManifest.xml
@@ -194,6 +194,11 @@
                 <action android:name="com.googlecode.android_scripting.service.FacadeService.ACTION_BIND" />
             </intent-filter>
         </service>
+        <service android:name=".facade.bluetooth.BluetoothAvrcpMediaBrowserService">
+            <intent-filter>
+                <action android:name="android.media.browse.MediaBrowserService"/>
+            </intent-filter>
+        </service>
         <activity android:name=".activity.InterpreterManager" android:launchMode="singleTask" android:configChanges="keyboardHidden|orientation" />
         <activity android:name=".activity.LogcatViewer" android:launchMode="singleTask" android:configChanges="keyboardHidden|orientation" />
         <activity android:name=".activity.ScriptsLiveFolder" android:label="Scripts" android:icon="@drawable/live_folder" android:configChanges="keyboardHidden|orientation">