Merge changes Ia4feba10,I8f8d11b2

* changes:
  Refactor A2dpSink and AvrcpController
  Move silent player to A2DPSink, get button routing only if focus is granted
diff --git a/jni/com_android_bluetooth_a2dp_sink.cpp b/jni/com_android_bluetooth_a2dp_sink.cpp
index 50c5087..91de5af 100644
--- a/jni/com_android_bluetooth_a2dp_sink.cpp
+++ b/jni/com_android_bluetooth_a2dp_sink.cpp
@@ -231,7 +231,7 @@
 
 int register_com_android_bluetooth_a2dp_sink(JNIEnv* env) {
   return jniRegisterNativeMethods(
-      env, "com/android/bluetooth/a2dpsink/A2dpSinkStateMachine", sMethods,
+      env, "com/android/bluetooth/a2dpsink/A2dpSinkService", sMethods,
       NELEM(sMethods));
 }
 }
diff --git a/jni/com_android_bluetooth_avrcp_controller.cpp b/jni/com_android_bluetooth_avrcp_controller.cpp
index 3815411..2d9e87b 100644
--- a/jni/com_android_bluetooth_avrcp_controller.cpp
+++ b/jni/com_android_bluetooth_avrcp_controller.cpp
@@ -71,7 +71,7 @@
   ScopedLocalRef<jbyteArray> addr(
       sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
   if (!addr.get()) {
-    ALOGE("Fail to new jbyteArray bd addr for passthrough response");
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
     return;
   }
 
@@ -109,7 +109,7 @@
   ScopedLocalRef<jbyteArray> addr(
       sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
   if (!addr.get()) {
-    ALOGE("Fail to new jbyteArray bd addr for connection state");
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
     return;
   }
 
@@ -134,7 +134,7 @@
   ScopedLocalRef<jbyteArray> addr(
       sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
   if (!addr.get()) {
-    ALOGE("Fail to new jbyteArray bd addr ");
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
     return;
   }
 
@@ -158,7 +158,7 @@
   ScopedLocalRef<jbyteArray> addr(
       sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
   if (!addr.get()) {
-    ALOGE("Fail to new jbyteArray bd addr ");
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
     return;
   }
 
@@ -184,7 +184,7 @@
   ScopedLocalRef<jbyteArray> addr(
       sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
   if (!addr.get()) {
-    ALOGE("Fail to new jbyteArray bd addr ");
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
     return;
   }
   sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
@@ -202,7 +202,7 @@
   ScopedLocalRef<jbyteArray> playerattribs(
       sCallbackEnv.get(), sCallbackEnv->NewByteArray(arraylen));
   if (!playerattribs.get()) {
-    ALOGE("Fail to new jbyteArray playerattribs ");
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
     return;
   }
 
@@ -236,7 +236,7 @@
   ScopedLocalRef<jbyteArray> addr(
       sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
   if (!addr.get()) {
-    ALOGE("Fail to get new array ");
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
     return;
   }
   sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
@@ -279,7 +279,7 @@
   ScopedLocalRef<jbyteArray> addr(
       sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
   if (!addr.get()) {
-    ALOGE("Fail to get new array ");
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
     return;
   }
 
@@ -303,7 +303,7 @@
   ScopedLocalRef<jbyteArray> addr(
       sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
   if (!addr.get()) {
-    ALOGE("Fail to get new array ");
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
     return;
   }
 
@@ -333,7 +333,7 @@
   ScopedLocalRef<jbyteArray> addr(
       sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
   if (!addr.get()) {
-    ALOGE("Fail to get new array ");
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
     return;
   }
 
@@ -388,7 +388,7 @@
   ScopedLocalRef<jbyteArray> addr(
       sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
   if (!addr.get()) {
-    ALOGE("Fail to get new array ");
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
     return;
   }
   sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
@@ -411,7 +411,7 @@
   ScopedLocalRef<jbyteArray> addr(
       sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
   if (!addr.get()) {
-    ALOGE("Fail to get new array ");
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
     return;
   }
   sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
@@ -436,6 +436,16 @@
     return;
   }
 
+  ScopedLocalRef<jbyteArray> addr(
+      sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+  if (!addr.get()) {
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
+    return;
+  }
+
+  sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+                                   (jbyte*)&bd_addr.address);
+
   // Inspect if the first element is a folder/item or player listing. They are
   // always exclusive.
   bool isPlayerListing =
@@ -468,17 +478,7 @@
           return;
         }
         // Parse UID
-        ScopedLocalRef<jbyteArray> uidByteArray(
-            sCallbackEnv.get(),
-            sCallbackEnv->NewByteArray(sizeof(uint8_t) * BTRC_UID_SIZE));
-        if (!uidByteArray.get()) {
-          ALOGE("%s can't allocate uid array!", __func__);
-          return;
-        }
-        sCallbackEnv->SetByteArrayRegion(uidByteArray.get(), 0,
-                                         BTRC_UID_SIZE * sizeof(uint8_t),
-                                         (jbyte*)item->media.uid);
-
+        long long uid = *(long long*)item->media.uid;
         // Parse Attrs
         ScopedLocalRef<jintArray> attrIdArray(
             sCallbackEnv.get(),
@@ -504,10 +504,6 @@
           ScopedLocalRef<jstring> attrValStr(
               sCallbackEnv.get(),
               sCallbackEnv->NewStringUTF((char*)(item->media.p_attrs[j].text)));
-          if (!uidByteArray.get()) {
-            ALOGE("%s can't allocate uid array!", __func__);
-            return;
-          }
           sCallbackEnv->SetObjectArrayElement(attrValArray.get(), j,
                                               attrValStr.get());
         }
@@ -515,9 +511,9 @@
         ScopedLocalRef<jobject> mediaObj(
             sCallbackEnv.get(),
             (jobject)sCallbackEnv->CallObjectMethod(
-                sCallbacksObj, method_createFromNativeMediaItem,
-                uidByteArray.get(), (jint)item->media.type, mediaName.get(),
-                attrIdArray.get(), attrValArray.get()));
+                sCallbacksObj, method_createFromNativeMediaItem, uid,
+                (jint)item->media.type, mediaName.get(), attrIdArray.get(),
+                attrValArray.get()));
         if (!mediaObj.get()) {
           ALOGE("%s failed to creae MediaItem for type ITEM_MEDIA", __func__);
           return;
@@ -536,22 +532,12 @@
           return;
         }
         // Parse UID
-        ScopedLocalRef<jbyteArray> uidByteArray(
-            sCallbackEnv.get(),
-            sCallbackEnv->NewByteArray(sizeof(uint8_t) * BTRC_UID_SIZE));
-        if (!uidByteArray.get()) {
-          ALOGE("%s can't allocate uid array!", __func__);
-          return;
-        }
-        sCallbackEnv->SetByteArrayRegion(uidByteArray.get(), 0,
-                                         BTRC_UID_SIZE * sizeof(uint8_t),
-                                         (jbyte*)item->folder.uid);
-
+        long long uid = *(long long*)item->folder.uid;
         ScopedLocalRef<jobject> folderObj(
             sCallbackEnv.get(),
             (jobject)sCallbackEnv->CallObjectMethod(
-                sCallbacksObj, method_createFromNativeFolderItem,
-                uidByteArray.get(), (jint)item->folder.type, folderName.get(),
+                sCallbacksObj, method_createFromNativeFolderItem, uid,
+                (jint)item->folder.type, folderName.get(),
                 (jint)item->folder.playable));
         if (!folderObj.get()) {
           ALOGE("%s failed to create MediaItem for type ITEM_FOLDER", __func__);
@@ -609,10 +595,10 @@
 
   if (isPlayerListing) {
     sCallbackEnv->CallVoidMethod(sCallbacksObj, method_handleGetPlayerItemsRsp,
-                                 itemArray.get());
+                                 addr.get(), itemArray.get());
   } else {
     sCallbackEnv->CallVoidMethod(sCallbacksObj, method_handleGetFolderItemsRsp,
-                                 status, itemArray.get());
+                                 addr.get(), status, itemArray.get());
   }
 }
 
@@ -626,9 +612,18 @@
     ALOGE("%s: sCallbacksObj is null", __func__);
     return;
   }
+  ScopedLocalRef<jbyteArray> addr(
+      sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+  if (!addr.get()) {
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
+    return;
+  }
+
+  sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+                                   (jbyte*)&bd_addr.address);
 
   sCallbackEnv->CallVoidMethod(sCallbacksObj, method_handleChangeFolderRsp,
-                               (jint)count);
+                               addr.get(), (jint)count);
 }
 
 static void btavrcp_set_browsed_player_callback(const RawAddress& bd_addr,
@@ -642,9 +637,18 @@
     ALOGE("%s: sCallbacksObj is null", __func__);
     return;
   }
+  ScopedLocalRef<jbyteArray> addr(
+      sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+  if (!addr.get()) {
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
+    return;
+  }
+
+  sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+                                   (jbyte*)&bd_addr.address);
 
   sCallbackEnv->CallVoidMethod(sCallbacksObj, method_handleSetBrowsedPlayerRsp,
-                               (jint)num_items, (jint)depth);
+                               addr.get(), (jint)num_items, (jint)depth);
 }
 
 static void btavrcp_set_addressed_player_callback(const RawAddress& bd_addr,
@@ -657,9 +661,19 @@
     ALOGE("%s: sCallbacksObj is null", __func__);
     return;
   }
+  ScopedLocalRef<jbyteArray> addr(
+      sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+  if (!addr.get()) {
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
+    return;
+  }
 
-  sCallbackEnv->CallVoidMethod(
-      sCallbacksObj, method_handleSetAddressedPlayerRsp, (jint)status);
+  sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+                                   (jbyte*)&bd_addr.address);
+
+  sCallbackEnv->CallVoidMethod(sCallbacksObj,
+                               method_handleSetAddressedPlayerRsp, addr.get(),
+                               (jint)status);
 }
 
 static void btavrcp_addressed_player_changed_callback(const RawAddress& bd_addr,
@@ -672,9 +686,18 @@
     ALOGE("%s: sCallbacksObj is null", __func__);
     return;
   }
+  ScopedLocalRef<jbyteArray> addr(
+      sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+  if (!addr.get()) {
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
+    return;
+  }
 
-  sCallbackEnv->CallVoidMethod(sCallbacksObj,
-                               method_handleAddressedPlayerChanged, (jint)id);
+  sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+                                   (jbyte*)&bd_addr.address);
+
+  sCallbackEnv->CallVoidMethod(
+      sCallbacksObj, method_handleAddressedPlayerChanged, addr.get(), (jint)id);
 }
 
 static void btavrcp_now_playing_content_changed_callback(
@@ -683,9 +706,18 @@
 
   CallbackEnv sCallbackEnv(__func__);
   if (!sCallbackEnv.valid()) return;
+  ScopedLocalRef<jbyteArray> addr(
+      sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+  if (!addr.get()) {
+    ALOGE("%s: Failed to allocate a new byte array", __func__);
+    return;
+  }
 
-  sCallbackEnv->CallVoidMethod(sCallbacksObj,
-                               method_handleNowPlayingContentChanged);
+  sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+                                   (jbyte*)&bd_addr.address);
+
+  sCallbackEnv->CallVoidMethod(
+      sCallbacksObj, method_handleNowPlayingContentChanged, addr.get());
 }
 
 static btrc_ctrl_callbacks_t sBluetoothAvrcpCallbacks = {
@@ -747,32 +779,32 @@
 
   method_handleGetFolderItemsRsp =
       env->GetMethodID(clazz, "handleGetFolderItemsRsp",
-                       "(I[Landroid/media/browse/MediaBrowser$MediaItem;)V");
+                       "([BI[Landroid/media/browse/MediaBrowser$MediaItem;)V");
   method_handleGetPlayerItemsRsp = env->GetMethodID(
       clazz, "handleGetPlayerItemsRsp",
-      "([Lcom/android/bluetooth/avrcpcontroller/AvrcpPlayer;)V");
+      "([B[Lcom/android/bluetooth/avrcpcontroller/AvrcpPlayer;)V");
 
   method_createFromNativeMediaItem =
       env->GetMethodID(clazz, "createFromNativeMediaItem",
-                       "([BILjava/lang/String;[I[Ljava/lang/String;)Landroid/"
+                       "(JILjava/lang/String;[I[Ljava/lang/String;)Landroid/"
                        "media/browse/MediaBrowser$MediaItem;");
   method_createFromNativeFolderItem = env->GetMethodID(
       clazz, "createFromNativeFolderItem",
-      "([BILjava/lang/String;I)Landroid/media/browse/MediaBrowser$MediaItem;");
+      "(JILjava/lang/String;I)Landroid/media/browse/MediaBrowser$MediaItem;");
   method_createFromNativePlayerItem =
       env->GetMethodID(clazz, "createFromNativePlayerItem",
                        "(ILjava/lang/String;[BII)Lcom/android/bluetooth/"
                        "avrcpcontroller/AvrcpPlayer;");
   method_handleChangeFolderRsp =
-      env->GetMethodID(clazz, "handleChangeFolderRsp", "(I)V");
+      env->GetMethodID(clazz, "handleChangeFolderRsp", "([BI)V");
   method_handleSetBrowsedPlayerRsp =
-      env->GetMethodID(clazz, "handleSetBrowsedPlayerRsp", "(II)V");
+      env->GetMethodID(clazz, "handleSetBrowsedPlayerRsp", "([BII)V");
   method_handleSetAddressedPlayerRsp =
-      env->GetMethodID(clazz, "handleSetAddressedPlayerRsp", "(I)V");
+      env->GetMethodID(clazz, "handleSetAddressedPlayerRsp", "([BI)V");
   method_handleAddressedPlayerChanged =
-      env->GetMethodID(clazz, "handleAddressedPlayerChanged", "(I)V");
+      env->GetMethodID(clazz, "handleAddressedPlayerChanged", "([BI)V");
   method_handleNowPlayingContentChanged =
-      env->GetMethodID(clazz, "handleNowPlayingContentChanged", "()V");
+      env->GetMethodID(clazz, "handleNowPlayingContentChanged", "([B)V");
 
   ALOGI("%s: succeeds", __func__);
 }
@@ -1081,7 +1113,7 @@
 
 static void changeFolderPathNative(JNIEnv* env, jobject object,
                                    jbyteArray address, jbyte direction,
-                                   jbyteArray uidarr) {
+                                   jlong uid) {
   if (!sBluetoothAvrcpInterface) return;
   jbyte* addr = env->GetByteArrayElements(address, NULL);
   if (!addr) {
@@ -1089,22 +1121,22 @@
     return;
   }
 
-  jbyte* uid = env->GetByteArrayElements(uidarr, NULL);
-  if (!uid) {
-    jniThrowIOException(env, EINVAL);
-    return;
-  }
+  // jbyte* uid = env->GetByteArrayElements(uidarr, NULL);
+  // if (!uid) {
+  //  jniThrowIOException(env, EINVAL);
+  //  return;
+  //}
 
   ALOGI("%s: sBluetoothAvrcpInterface: %p", __func__, sBluetoothAvrcpInterface);
   RawAddress rawAddress;
   rawAddress.FromOctets((uint8_t*)addr);
 
   bt_status_t status = sBluetoothAvrcpInterface->change_folder_path_cmd(
-      rawAddress, (uint8_t)direction, (uint8_t*)uid);
+      rawAddress, (uint8_t)direction, (uint8_t*)&uid);
   if (status != BT_STATUS_SUCCESS) {
     ALOGE("Failed sending changeFolderPathNative command, status: %d", status);
   }
-  env->ReleaseByteArrayElements(address, addr, 0);
+  // env->ReleaseByteArrayElements(address, addr, 0);
 }
 
 static void setBrowsedPlayerNative(JNIEnv* env, jobject object,
@@ -1149,7 +1181,7 @@
 }
 
 static void playItemNative(JNIEnv* env, jobject object, jbyteArray address,
-                           jbyte scope, jbyteArray uidArr, jint uidCounter) {
+                           jbyte scope, jlong uid, jint uidCounter) {
   if (!sBluetoothAvrcpInterface) return;
   jbyte* addr = env->GetByteArrayElements(address, NULL);
   if (!addr) {
@@ -1157,17 +1189,17 @@
     return;
   }
 
-  jbyte* uid = env->GetByteArrayElements(uidArr, NULL);
-  if (!uid) {
-    jniThrowIOException(env, EINVAL);
-    return;
-  }
+  //  jbyte* uid = env->GetByteArrayElements(uidArr, NULL);
+  //  if (!uid) {
+  //    jniThrowIOException(env, EINVAL);
+  //    return;
+  //  }
   RawAddress rawAddress;
   rawAddress.FromOctets((uint8_t*)addr);
 
   ALOGI("%s: sBluetoothAvrcpInterface: %p", __func__, sBluetoothAvrcpInterface);
   bt_status_t status = sBluetoothAvrcpInterface->play_item_cmd(
-      rawAddress, (uint8_t)scope, (uint8_t*)uid, (uint16_t)uidCounter);
+      rawAddress, (uint8_t)scope, (uint8_t*)&uid, (uint16_t)uidCounter);
   if (status != BT_STATUS_SUCCESS) {
     ALOGE("Failed sending playItemNative command, status: %d", status);
   }
@@ -1191,8 +1223,8 @@
     {"getNowPlayingListNative", "([BII)V", (void*)getNowPlayingListNative},
     {"getFolderListNative", "([BII)V", (void*)getFolderListNative},
     {"getPlayerListNative", "([BII)V", (void*)getPlayerListNative},
-    {"changeFolderPathNative", "([BB[B)V", (void*)changeFolderPathNative},
-    {"playItemNative", "([BB[BI)V", (void*)playItemNative},
+    {"changeFolderPathNative", "([BBJ)V", (void*)changeFolderPathNative},
+    {"playItemNative", "([BBJI)V", (void*)playItemNative},
     {"setBrowsedPlayerNative", "([BI)V", (void*)setBrowsedPlayerNative},
     {"setAddressedPlayerNative", "([BI)V", (void*)setAddressedPlayerNative},
 };
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java
index 8292b53..e951ed5 100644
--- a/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java
@@ -13,222 +13,94 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package com.android.bluetooth.a2dpsink;
 
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothAudioConfig;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.IBluetoothA2dpSink;
-import android.content.Intent;
 import android.util.Log;
 
 import com.android.bluetooth.Utils;
-import com.android.bluetooth.avrcpcontroller.AvrcpControllerService;
-import com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService;
 import com.android.bluetooth.btservice.AdapterService;
 import com.android.bluetooth.btservice.ProfileService;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * Provides Bluetooth A2DP Sink profile, as a service in the Bluetooth application.
  * @hide
  */
 public class A2dpSinkService extends ProfileService {
-    private static final boolean DBG = true;
     private static final String TAG = "A2dpSinkService";
+    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+    static final int MAXIMUM_CONNECTED_DEVICES = 1;
 
-    private A2dpSinkStateMachine mStateMachine;
-    private static A2dpSinkService sA2dpSinkService;
+    private final BluetoothAdapter mAdapter;
+    protected Map<BluetoothDevice, A2dpSinkStateMachine> mDeviceStateMap =
+            new ConcurrentHashMap<>(1);
 
-    @Override
-    protected IProfileServiceBinder initBinder() {
-        return new BluetoothA2dpSinkBinder(this);
+    private A2dpSinkStreamHandler mA2dpSinkStreamHandler;
+    private static A2dpSinkService sService;
+
+    static {
+        classInitNative();
     }
 
     @Override
     protected boolean start() {
-        if (DBG) {
-            Log.d(TAG, "start()");
-        }
-        // Start the media browser service.
-        Intent startIntent = new Intent(this, BluetoothMediaBrowserService.class);
-        startService(startIntent);
-        mStateMachine = A2dpSinkStateMachine.make(this, this);
-        setA2dpSinkService(this);
+        initNative();
+        sService = this;
+        mA2dpSinkStreamHandler = new A2dpSinkStreamHandler(this, this);
         return true;
     }
 
     @Override
     protected boolean stop() {
-        if (DBG) {
-            Log.d(TAG, "stop()");
+        for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) {
+            stateMachine.quitNow();
         }
-        setA2dpSinkService(null);
-        if (mStateMachine != null) {
-            mStateMachine.doQuit();
-        }
-        Intent stopIntent = new Intent(this, BluetoothMediaBrowserService.class);
-        stopService(stopIntent);
+        sService = null;
         return true;
     }
 
+    public static A2dpSinkService getA2dpSinkService() {
+        return sService;
+    }
+
+    public A2dpSinkService() {
+        mAdapter = BluetoothAdapter.getDefaultAdapter();
+    }
+
+    protected A2dpSinkStateMachine newStateMachine(BluetoothDevice device) {
+        return new A2dpSinkStateMachine(device, this);
+    }
+
+    protected synchronized A2dpSinkStateMachine getStateMachine(BluetoothDevice device) {
+        return mDeviceStateMap.get(device);
+    }
+
+    /**
+     * Request audio focus such that the designated device can stream audio
+     */
+    public void requestAudioFocus(BluetoothDevice device, boolean request) {
+        mA2dpSinkStreamHandler.requestAudioFocus(request);
+    }
+
     @Override
-    protected void cleanup() {
-        if (mStateMachine != null) {
-            mStateMachine.cleanup();
-        }
-    }
-
-    //API Methods
-
-    public static synchronized A2dpSinkService getA2dpSinkService() {
-        if (sA2dpSinkService == null) {
-            Log.w(TAG, "getA2dpSinkService(): service is null");
-            return null;
-        }
-        if (!sA2dpSinkService.isAvailable()) {
-            Log.w(TAG, "getA2dpSinkService(): service is not available ");
-            return null;
-        }
-        return sA2dpSinkService;
-    }
-
-    private static synchronized void setA2dpSinkService(A2dpSinkService instance) {
-        if (DBG) {
-            Log.d(TAG, "setA2dpSinkService(): set to: " + instance);
-        }
-        sA2dpSinkService = instance;
-    }
-
-    public boolean connect(BluetoothDevice device) {
-        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
-
-        int connectionState = mStateMachine.getConnectionState(device);
-        if (connectionState == BluetoothProfile.STATE_CONNECTED
-                || connectionState == BluetoothProfile.STATE_CONNECTING) {
-            return false;
-        }
-
-        if (getPriority(device) == BluetoothProfile.PRIORITY_OFF) {
-            return false;
-        }
-
-        mStateMachine.sendMessage(A2dpSinkStateMachine.CONNECT, device);
-        return true;
-    }
-
-    boolean disconnect(BluetoothDevice device) {
-        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
-        int connectionState = mStateMachine.getConnectionState(device);
-        if (connectionState != BluetoothProfile.STATE_CONNECTED
-                && connectionState != BluetoothProfile.STATE_CONNECTING) {
-            return false;
-        }
-
-        mStateMachine.sendMessage(A2dpSinkStateMachine.DISCONNECT, device);
-        return true;
-    }
-
-    public List<BluetoothDevice> getConnectedDevices() {
-        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-        return mStateMachine.getConnectedDevices();
-    }
-
-    List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
-        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-        return mStateMachine.getDevicesMatchingConnectionStates(states);
-    }
-
-    int getConnectionState(BluetoothDevice device) {
-        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-        return mStateMachine.getConnectionState(device);
-    }
-
-    public boolean setPriority(BluetoothDevice device, int priority) {
-        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
-        if (DBG) {
-            Log.d(TAG, "Saved priority " + device + " = " + priority);
-        }
-        AdapterService.getAdapterService().getDatabase()
-                .setProfilePriority(device, BluetoothProfile.A2DP_SINK, priority);
-        return true;
-    }
-
-    public int getPriority(BluetoothDevice device) {
-        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
-        return AdapterService.getAdapterService().getDatabase()
-                .getProfilePriority(device, BluetoothProfile.A2DP_SINK);
-    }
-
-    /**
-     * Called by AVRCP controller to provide information about the last user intent on CT.
-     *
-     * If the user has pressed play in the last attempt then A2DP Sink component will grant focus to
-     * any incoming sound from the phone (and also retain focus for a few seconds before
-     * relinquishing. On the other hand if the user has pressed pause/stop then the A2DP sink
-     * component will take the focus away but also notify the stack to throw away incoming data.
-     */
-    public void informAvrcpPassThroughCmd(BluetoothDevice device, int keyCode, int keyState) {
-        if (mStateMachine != null) {
-            if (keyCode == AvrcpControllerService.PASS_THRU_CMD_ID_PLAY
-                    && keyState == AvrcpControllerService.KEY_STATE_RELEASED) {
-                mStateMachine.sendMessage(A2dpSinkStateMachine.EVENT_AVRCP_CT_PLAY);
-            } else if ((keyCode == AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE
-                    || keyCode == AvrcpControllerService.PASS_THRU_CMD_ID_STOP)
-                    && keyState == AvrcpControllerService.KEY_STATE_RELEASED) {
-                mStateMachine.sendMessage(A2dpSinkStateMachine.EVENT_AVRCP_CT_PAUSE);
-            }
-        }
-    }
-
-    /**
-     * Called by AVRCP controller to provide information about the last user intent on TG.
-     *
-     * Tf the user has pressed pause on the TG then we can preempt streaming music. This is opposed
-     * to when the streaming stops abruptly (jitter) in which case we will wait for sometime before
-     * stopping playback.
-     */
-    public void informTGStatePlaying(BluetoothDevice device, boolean isPlaying) {
-        if (mStateMachine != null) {
-            if (!isPlaying) {
-                mStateMachine.sendMessage(A2dpSinkStateMachine.EVENT_AVRCP_TG_PAUSE);
-            } else {
-                mStateMachine.sendMessage(A2dpSinkStateMachine.EVENT_AVRCP_TG_PLAY);
-            }
-        }
-    }
-
-    /**
-     * Called by AVRCP controller to establish audio focus.
-     *
-     * In order to perform streaming the A2DP sink must have audio focus.  This interface allows the
-     * associated MediaSession to inform the sink of intent to play and then allows streaming to be
-     * started from either the source or the sink endpoint.
-     */
-    public void requestAudioFocus(BluetoothDevice device, boolean request) {
-        if (mStateMachine != null) {
-            mStateMachine.sendMessage(A2dpSinkStateMachine.EVENT_REQUEST_FOCUS);
-        }
-    }
-
-    synchronized boolean isA2dpPlaying(BluetoothDevice device) {
-        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-        if (DBG) {
-            Log.d(TAG, "isA2dpPlaying(" + device + ")");
-        }
-        return mStateMachine.isPlaying(device);
-    }
-
-    BluetoothAudioConfig getAudioConfig(BluetoothDevice device) {
-        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-        return mStateMachine.getAudioConfig(device);
+    protected IProfileServiceBinder initBinder() {
+        return new A2dpSinkServiceBinder(this);
     }
 
     //Binder object: Must be static class or memory leak may occur
-    private static class BluetoothA2dpSinkBinder extends IBluetoothA2dpSink.Stub
+    private static class A2dpSinkServiceBinder extends IBluetoothA2dpSink.Stub
             implements IProfileServiceBinder {
         private A2dpSinkService mService;
 
@@ -238,13 +110,13 @@
                 return null;
             }
 
-            if (mService != null && mService.isAvailable()) {
+            if (mService != null) {
                 return mService;
             }
             return null;
         }
 
-        BluetoothA2dpSinkBinder(A2dpSinkService svc) {
+        A2dpSinkServiceBinder(A2dpSinkService svc) {
             mService = svc;
         }
 
@@ -299,15 +171,6 @@
         }
 
         @Override
-        public boolean isA2dpPlaying(BluetoothDevice device) {
-            A2dpSinkService service = getService();
-            if (service == null) {
-                return false;
-            }
-            return service.isA2dpPlaying(device);
-        }
-
-        @Override
         public boolean setPriority(BluetoothDevice device, int priority) {
             A2dpSinkService service = getService();
             if (service == null) {
@@ -326,6 +189,15 @@
         }
 
         @Override
+        public boolean isA2dpPlaying(BluetoothDevice device) {
+            A2dpSinkService service = getService();
+            if (service == null) {
+                return false;
+            }
+            return service.isA2dpPlaying(device);
+        }
+
+        @Override
         public BluetoothAudioConfig getAudioConfig(BluetoothDevice device) {
             A2dpSinkService service = getService();
             if (service == null) {
@@ -335,13 +207,219 @@
         }
     }
 
-    ;
+    /* Generic Profile Code */
+
+    /**
+     * Connect the given Bluetooth device.
+     *
+     * @return true if connection is successful, false otherwise.
+     */
+    public synchronized boolean connect(BluetoothDevice device) {
+        if (device == null) {
+            throw new IllegalArgumentException("Null device");
+        }
+        if (DBG) {
+            StringBuilder sb = new StringBuilder();
+            dump(sb);
+            Log.d(TAG, " connect device: " + device
+                    + ", InstanceMap start state: " + sb.toString());
+        }
+        A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device);
+        if (stateMachine != null) {
+            stateMachine.connect();
+            return true;
+        } else {
+            // a state machine instance doesn't exist yet, and the max has been reached.
+            Log.e(TAG, "Maxed out on the number of allowed MAP connections. "
+                    + "Connect request rejected on " + device);
+            return false;
+
+        }
+    }
+
+    /**
+     * Disconnect the given Bluetooth device.
+     *
+     * @return true if disconnect is successful, false otherwise.
+     */
+    public synchronized boolean disconnect(BluetoothDevice device) {
+        if (DBG) {
+            StringBuilder sb = new StringBuilder();
+            dump(sb);
+            Log.d(TAG, "A2DP disconnect device: " + device
+                    + ", InstanceMap start state: " + sb.toString());
+        }
+        A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
+        // a state machine instance doesn't exist. maybe it is already gone?
+        if (stateMachine == null) {
+            return false;
+        }
+        int connectionState = stateMachine.getState();
+        if (connectionState == BluetoothProfile.STATE_DISCONNECTED
+                || connectionState == BluetoothProfile.STATE_DISCONNECTING) {
+            return false;
+        }
+        // upon completion of disconnect, the state machine will remove itself from the available
+        // devices map
+        stateMachine.disconnect();
+        return true;
+    }
+
+    void removeStateMachine(A2dpSinkStateMachine stateMachine) {
+        mDeviceStateMap.remove(stateMachine.getDevice());
+    }
+
+    public List<BluetoothDevice> getConnectedDevices() {
+        return getDevicesMatchingConnectionStates(new int[]{BluetoothAdapter.STATE_CONNECTED});
+    }
+
+    protected A2dpSinkStateMachine getOrCreateStateMachine(BluetoothDevice device) {
+        A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
+        if (stateMachine == null) {
+            stateMachine = newStateMachine(device);
+            mDeviceStateMap.put(device, stateMachine);
+            stateMachine.start();
+        }
+        return stateMachine;
+    }
+
+    List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+        if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states));
+        List<BluetoothDevice> deviceList = new ArrayList<>();
+        Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices();
+        int connectionState;
+        for (BluetoothDevice device : bondedDevices) {
+            connectionState = getConnectionState(device);
+            if (DBG) Log.d(TAG, "Device: " + device + "State: " + connectionState);
+            for (int i = 0; i < states.length; i++) {
+                if (connectionState == states[i]) {
+                    deviceList.add(device);
+                }
+            }
+        }
+        if (DBG) Log.d(TAG, deviceList.toString());
+        Log.d(TAG, "GetDevicesDone");
+        return deviceList;
+    }
+
+    synchronized int getConnectionState(BluetoothDevice device) {
+        A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
+        return (stateMachine == null) ? BluetoothProfile.STATE_DISCONNECTED
+                : stateMachine.getState();
+    }
+
+    /**
+     * Set the priority of the  profile.
+     *
+     * @param device   the remote device
+     * @param priority the priority of the profile
+     * @return true on success, otherwise false
+     */
+    public boolean setPriority(BluetoothDevice device, int priority) {
+        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
+        if (DBG) {
+            Log.d(TAG, "Saved priority " + device + " = " + priority);
+        }
+        AdapterService.getAdapterService().getDatabase()
+                .setProfilePriority(device, BluetoothProfile.A2DP_SINK, priority);
+        return true;
+    }
+
+    /**
+     * Get the priority of the profile.
+     *
+     * @param device the remote device
+     * @return priority of the specified device
+     */
+    public int getPriority(BluetoothDevice device) {
+        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
+        return AdapterService.getAdapterService().getDatabase()
+                .getProfilePriority(device, BluetoothProfile.A2DP_SINK);
+    }
+
 
     @Override
     public void dump(StringBuilder sb) {
         super.dump(sb);
-        if (mStateMachine != null) {
-            mStateMachine.dump(sb);
+        ProfileService.println(sb, "Devices Tracked = " + mDeviceStateMap.size());
+        for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) {
+            ProfileService.println(sb,
+                    "==== StateMachine for " + stateMachine.getDevice() + " ====");
+            stateMachine.dump(sb);
         }
     }
+
+    /**
+     * Get the current Bluetooth Audio focus state
+     *
+     * @return focus
+     */
+    public static int getFocusState() {
+        return sService.mA2dpSinkStreamHandler.getFocusState();
+    }
+
+    boolean isA2dpPlaying(BluetoothDevice device) {
+        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+        return mA2dpSinkStreamHandler.isPlaying();
+    }
+
+    BluetoothAudioConfig getAudioConfig(BluetoothDevice device) {
+        A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
+        // a state machine instance doesn't exist. maybe it is already gone?
+        if (stateMachine == null) {
+            return null;
+        }
+        return stateMachine.getAudioConfig();
+    }
+
+    /* JNI interfaces*/
+
+    private static native void classInitNative();
+
+    private native void initNative();
+
+    private native void cleanupNative();
+
+    native boolean connectA2dpNative(byte[] address);
+
+    native boolean disconnectA2dpNative(byte[] address);
+
+    /**
+     * inform A2DP decoder of the current audio focus
+     *
+     * @param focusGranted
+     */
+    @VisibleForTesting
+    public native void informAudioFocusStateNative(int focusGranted);
+
+    /**
+     * inform A2DP decoder the desired audio gain
+     *
+     * @param gain
+     */
+    @VisibleForTesting
+    public native void informAudioTrackGainNative(float gain);
+
+    private void onConnectionStateChanged(byte[] address, int state) {
+        StackEvent event = StackEvent.connectionStateChanged(getDevice(address), state);
+        A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(event.mDevice);
+        stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event);
+    }
+
+    private void onAudioStateChanged(byte[] address, int state) {
+        if (state == StackEvent.AUDIO_STATE_STARTED) {
+            mA2dpSinkStreamHandler.obtainMessage(
+                    A2dpSinkStreamHandler.SRC_STR_START).sendToTarget();
+        } else if (state == StackEvent.AUDIO_STATE_STOPPED) {
+            mA2dpSinkStreamHandler.obtainMessage(
+                    A2dpSinkStreamHandler.SRC_STR_STOP).sendToTarget();
+        }
+    }
+
+    private void onAudioConfigChanged(byte[] address, int sampleRate, int channelCount) {
+        StackEvent event = StackEvent.audioConfigChanged(getDevice(address), sampleRate,
+                channelCount);
+        A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(event.mDevice);
+        stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event);
+    }
 }
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
index ed77281..19ed87f 100644
--- a/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
@@ -13,888 +13,288 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-/**
- * Bluetooth A2dp Sink StateMachine
- *                      (Disconnected)
- *                           |    ^
- *                   CONNECT |    | DISCONNECTED
- *                           V    |
- *                         (Pending)
- *                           |    ^
- *                 CONNECTED |    | CONNECT
- *                           V    |
- *                        (Connected -- See A2dpSinkStreamHandler)
- */
 package com.android.bluetooth.a2dpsink;
 
 import android.bluetooth.BluetoothA2dpSink;
-import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothAudioConfig;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
-import android.bluetooth.BluetoothUuid;
-import android.content.Context;
 import android.content.Intent;
 import android.media.AudioFormat;
-import android.media.AudioManager;
-import android.os.Handler;
 import android.os.Message;
-import android.os.ParcelUuid;
-import android.os.PowerManager;
 import android.util.Log;
 
 import com.android.bluetooth.BluetoothMetricsProto;
 import com.android.bluetooth.Utils;
-import com.android.bluetooth.avrcpcontroller.AvrcpControllerService;
-import com.android.bluetooth.btservice.AdapterService;
 import com.android.bluetooth.btservice.MetricsLogger;
 import com.android.bluetooth.btservice.ProfileService;
-import com.android.internal.util.IState;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Set;
 
 public class A2dpSinkStateMachine extends StateMachine {
-    private static final boolean DBG = false;
+    static final String TAG = "A2DPSinkStateMachine";
+    static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
 
-    static final int CONNECT = 1;
-    static final int DISCONNECT = 2;
-    private static final int STACK_EVENT = 101;
-    private static final int CONNECT_TIMEOUT = 201;
-    public static final int EVENT_AVRCP_CT_PLAY = 301;
-    public static final int EVENT_AVRCP_CT_PAUSE = 302;
-    public static final int EVENT_AVRCP_TG_PLAY = 303;
-    public static final int EVENT_AVRCP_TG_PAUSE = 304;
-    public static final int EVENT_REQUEST_FOCUS = 305;
+    //0->99 Events from Outside
+    public static final int CONNECT = 1;
+    public static final int DISCONNECT = 2;
 
-    private static final int IS_INVALID_DEVICE = 0;
-    private static final int IS_VALID_DEVICE = 1;
-    private static final int CONNECT_TIMEOUT_MS = 5000;
-    public static final int AVRC_ID_PLAY = 0x44;
-    public static final int AVRC_ID_PAUSE = 0x46;
-    public static final int KEY_STATE_PRESSED = 0;
-    public static final int KEY_STATE_RELEASED = 1;
+    //100->199 Internal Events
+    protected static final int CLEANUP = 100;
+    private static final int CONNECT_TIMEOUT = 101;
 
-    // Connection states.
-    // 1. Disconnected: The connection does not exist.
-    // 2. Pending: The connection is being established.
-    // 3. Connected: The connection is established. The audio connection is in Idle state.
-    private Disconnected mDisconnected;
-    private Pending mPending;
-    private Connected mConnected;
+    //200->299 Events from Native
+    static final int STACK_EVENT = 200;
 
-    private A2dpSinkService mService;
-    private Context mContext;
-    private BluetoothAdapter mAdapter;
-    private IntentBroadcastHandler mIntentBroadcastHandler;
+    static final int CONNECT_TIMEOUT_MS = 5000;
 
-    private static final int MSG_CONNECTION_STATE_CHANGED = 0;
+    protected final BluetoothDevice mDevice;
+    protected final byte[] mDeviceAddress;
+    protected final A2dpSinkService mService;
+    protected final Disconnected mDisconnected;
+    protected final Connecting mConnecting;
+    protected final Connected mConnected;
+    protected final Disconnecting mDisconnecting;
 
-    private final Object mLockForPatch = new Object();
+    protected int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
+    protected BluetoothAudioConfig mAudioConfig = null;
 
-    // mCurrentDevice is the device connected before the state changes
-    // mTargetDevice is the device to be connected
-    // mIncomingDevice is the device connecting to us, valid only in Pending state
-    //                when mIncomingDevice is not null, both mCurrentDevice
-    //                  and mTargetDevice are null
-    //                when either mCurrentDevice or mTargetDevice is not null,
-    //                  mIncomingDevice is null
-    // Stable states
-    //   No connection, Disconnected state
-    //                  both mCurrentDevice and mTargetDevice are null
-    //   Connected, Connected state
-    //              mCurrentDevice is not null, mTargetDevice is null
-    // Interim states
-    //   Connecting to a device, Pending
-    //                           mCurrentDevice is null, mTargetDevice is not null
-    //   Disconnecting device, Connecting to new device
-    //     Pending
-    //     Both mCurrentDevice and mTargetDevice are not null
-    //   Disconnecting device Pending
-    //                        mCurrentDevice is not null, mTargetDevice is null
-    //   Incoming connections Pending
-    //                        Both mCurrentDevice and mTargetDevice are null
-    private BluetoothDevice mCurrentDevice = null;
-    private BluetoothDevice mTargetDevice = null;
-    private BluetoothDevice mIncomingDevice = null;
-    private BluetoothDevice mPlayingDevice = null;
-    private A2dpSinkStreamHandler mStreaming = null;
-
-    private final HashMap<BluetoothDevice, BluetoothAudioConfig> mAudioConfigs =
-            new HashMap<BluetoothDevice, BluetoothAudioConfig>();
-
-    static {
-        classInitNative();
-    }
-
-    private A2dpSinkStateMachine(A2dpSinkService svc, Context context) {
-        super("A2dpSinkStateMachine");
-        mService = svc;
-        mContext = context;
-        mAdapter = BluetoothAdapter.getDefaultAdapter();
-
-        initNative();
+    A2dpSinkStateMachine(BluetoothDevice device, A2dpSinkService service) {
+        super(TAG);
+        mDevice = device;
+        mDeviceAddress = Utils.getByteAddress(mDevice);
+        mService = service;
+        if (DBG) Log.d(TAG, device.toString());
 
         mDisconnected = new Disconnected();
-        mPending = new Pending();
+        mConnecting = new Connecting();
         mConnected = new Connected();
+        mDisconnecting = new Disconnecting();
 
         addState(mDisconnected);
-        addState(mPending);
+        addState(mConnecting);
         addState(mConnected);
+        addState(mDisconnecting);
 
         setInitialState(mDisconnected);
-
-        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
-
-        mIntentBroadcastHandler = new IntentBroadcastHandler();
     }
 
-    static A2dpSinkStateMachine make(A2dpSinkService svc, Context context) {
-        Log.d("A2dpSinkStateMachine", "make");
-        A2dpSinkStateMachine a2dpSm = new A2dpSinkStateMachine(svc, context);
-        a2dpSm.start();
-        return a2dpSm;
+    protected String getConnectionStateChangedIntent() {
+        return BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED;
     }
 
-    public void doQuit() {
-        if (DBG) {
-            Log.d("A2dpSinkStateMachine", "Quit");
-        }
-        synchronized (A2dpSinkStateMachine.this) {
-            mStreaming = null;
-        }
-        quitNow();
+    /**
+     * Get the current connection state
+     *
+     * @return current State
+     */
+    public int getState() {
+        return mMostRecentState;
     }
 
-    public void cleanup() {
-        cleanupNative();
-        mAudioConfigs.clear();
+    /**
+     * get current audio config
+     */
+    BluetoothAudioConfig getAudioConfig() {
+        return mAudioConfig;
     }
 
+    /**
+     * Get the underlying device tracked by this state machine
+     *
+     * @return device in focus
+     */
+    public synchronized BluetoothDevice getDevice() {
+        return mDevice;
+    }
+
+    /**
+     * send the Connect command asynchronously
+     */
+    public final void connect() {
+        sendMessage(CONNECT);
+    }
+
+    /**
+     * send the Disconnect command asynchronously
+     */
+    public final void disconnect() {
+        sendMessage(DISCONNECT);
+    }
+
+    /**
+     * Dump the current State Machine to the string builder.
+     * @param sb output string
+     */
     public void dump(StringBuilder sb) {
-        if (mCurrentDevice == null) return;
-        ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice.getAddress() + "("
-                + mCurrentDevice.getName() + ") " + this.toString());
-        ProfileService.println(sb, "mTargetDevice: " + mTargetDevice);
-        ProfileService.println(sb, "mIncomingDevice: " + mIncomingDevice);
+        ProfileService.println(sb, "mDevice: " + mDevice.getAddress() + "("
+                + mDevice.getName() + ") " + this.toString());
     }
 
-    private class Disconnected extends State {
+    @Override
+    protected void unhandledMessage(Message msg) {
+        Log.w(TAG, "unhandledMessage in state " + getCurrentState() + "msg.what=" + msg.what);
+    }
+
+    class Disconnected extends State {
         @Override
         public void enter() {
-            log("Enter Disconnected: " + getCurrentMessage().what);
+            if (DBG) Log.d(TAG, "Enter Disconnected");
+            if (mMostRecentState != BluetoothProfile.STATE_DISCONNECTED) {
+                sendMessage(CLEANUP);
+            }
+            onConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTED);
         }
 
         @Override
         public boolean processMessage(Message message) {
-            log("Disconnected process message: " + message.what);
-            if (mCurrentDevice != null || mTargetDevice != null || mIncomingDevice != null) {
-                loge("ERROR: current, target, or mIncomingDevice not null in Disconnected");
-                return NOT_HANDLED;
-            }
-
-            boolean retValue = HANDLED;
             switch (message.what) {
-                case CONNECT:
-                    BluetoothDevice device = (BluetoothDevice) message.obj;
-                    broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING,
-                            BluetoothProfile.STATE_DISCONNECTED);
-
-                    if (!connectA2dpNative(getByteAddress(device))) {
-                        broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
-                                BluetoothProfile.STATE_CONNECTING);
-                        break;
-                    }
-
-                    synchronized (A2dpSinkStateMachine.this) {
-                        mTargetDevice = device;
-                        transitionTo(mPending);
-                    }
-                    break;
-                case DISCONNECT:
-                    // ignore
-                    break;
                 case STACK_EVENT:
-                    StackEvent event = (StackEvent) message.obj;
-                    switch (event.type) {
-                        case EVENT_TYPE_CONNECTION_STATE_CHANGED:
-                            processConnectionEvent(event.device, event.valueInt);
-                            break;
-                        case EVENT_TYPE_AUDIO_CONFIG_CHANGED:
-                            processAudioConfigEvent(event.device, event.audioConfig);
-                            break;
-                        default:
-                            loge("Unexpected stack event: " + event.type);
-                            break;
-                    }
-                    break;
-                default:
-                    return NOT_HANDLED;
+                    processStackEvent((StackEvent) message.obj);
+                    return true;
+                case CONNECT:
+                    if (DBG) Log.d(TAG, "Connect");
+                    transitionTo(mConnecting);
+                    return true;
+                case CLEANUP:
+                    mService.removeStateMachine(A2dpSinkStateMachine.this);
+                    return true;
             }
-            return retValue;
+            return false;
         }
 
-        @Override
-        public void exit() {
-            log("Exit Disconnected: " + getCurrentMessage().what);
-        }
-
-        // in Disconnected state
-        private void processConnectionEvent(BluetoothDevice device, int state) {
-            switch (state) {
-                case CONNECTION_STATE_DISCONNECTED:
-                    logw("Ignore A2DP DISCONNECTED event, device: " + device);
-                    break;
-                case CONNECTION_STATE_CONNECTING:
-                    if (okToConnect(device)) {
-                        logi("Incoming A2DP accepted");
-                        broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING,
-                                BluetoothProfile.STATE_DISCONNECTED);
-                        synchronized (A2dpSinkStateMachine.this) {
-                            mIncomingDevice = device;
-                            transitionTo(mPending);
-                        }
-                    } else {
-                        //reject the connection and stay in Disconnected state itself
-                        logi("Incoming A2DP rejected");
-                        disconnectA2dpNative(getByteAddress(device));
-                    }
-                    break;
-                case CONNECTION_STATE_CONNECTED:
-                    logw("A2DP Connected from Disconnected state");
-                    if (okToConnect(device)) {
-                        logi("Incoming A2DP accepted");
-                        broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED,
-                                BluetoothProfile.STATE_DISCONNECTED);
-                        synchronized (A2dpSinkStateMachine.this) {
-                            mCurrentDevice = device;
+        void processStackEvent(StackEvent event) {
+            switch (event.mType) {
+                case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
+                    switch (event.mState) {
+                        case StackEvent.CONNECTION_STATE_CONNECTED:
                             transitionTo(mConnected);
-                        }
-                    } else {
-                        //reject the connection and stay in Disconnected state itself
-                        logi("Incoming A2DP rejected");
-                        disconnectA2dpNative(getByteAddress(device));
+                            break;
+                        case StackEvent.CONNECTION_STATE_DISCONNECTED:
+                            sendMessage(CLEANUP);
+                            break;
                     }
-                    break;
-                case CONNECTION_STATE_DISCONNECTING:
-                    logw("Ignore HF DISCONNECTING event, device: " + device);
-                    break;
-                default:
-                    loge("Incorrect state: " + state);
-                    break;
             }
         }
     }
 
-    private class Pending extends State {
+    class Connecting extends State {
+        boolean mIncommingConnection = false;
+
         @Override
         public void enter() {
-            log("Enter Pending: " + getCurrentMessage().what);
+            if (DBG) Log.d(TAG, "Enter Connecting");
+            onConnectionStateChanged(BluetoothProfile.STATE_CONNECTING);
             sendMessageDelayed(CONNECT_TIMEOUT, CONNECT_TIMEOUT_MS);
+
+            if (!mIncommingConnection) {
+                mService.connectA2dpNative(mDeviceAddress);
+            }
+
+            super.enter();
         }
 
         @Override
         public boolean processMessage(Message message) {
-            log("Pending process message: " + message.what);
-
-            boolean retValue = HANDLED;
             switch (message.what) {
-                case CONNECT:
-                    logd("Disconnect before connecting to another target");
-                    break;
-                case CONNECT_TIMEOUT:
-                    onConnectionStateChanged(getByteAddress(mTargetDevice),
-                                             CONNECTION_STATE_DISCONNECTED);
-                    break;
-                case DISCONNECT:
-                    BluetoothDevice device = (BluetoothDevice) message.obj;
-                    if (mCurrentDevice != null && mTargetDevice != null && mTargetDevice.equals(
-                            device)) {
-                        // cancel connection to the mTargetDevice
-                        broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
-                                BluetoothProfile.STATE_CONNECTING);
-                        synchronized (A2dpSinkStateMachine.this) {
-                            mTargetDevice = null;
-                        }
-                    }
-                    break;
                 case STACK_EVENT:
-                    StackEvent event = (StackEvent) message.obj;
-                    log("STACK_EVENT " + event.type);
-                    switch (event.type) {
-                        case EVENT_TYPE_CONNECTION_STATE_CHANGED:
-                            processConnectionEvent(event.device, event.valueInt);
-                            break;
-                        case EVENT_TYPE_AUDIO_CONFIG_CHANGED:
-                            processAudioConfigEvent(event.device, event.audioConfig);
-                            break;
-                        default:
-                            loge("Unexpected stack event: " + event.type);
-                            break;
-                    }
-                    break;
-                default:
-                    return NOT_HANDLED;
+                    processStackEvent((StackEvent) message.obj);
+                    return true;
+                case CONNECT_TIMEOUT:
+                    transitionTo(mDisconnected);
+                    return true;
             }
-            return retValue;
+            return false;
         }
 
+        void processStackEvent(StackEvent event) {
+            switch (event.mType) {
+                case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
+                    switch (event.mState) {
+                        case StackEvent.CONNECTION_STATE_CONNECTED:
+                            transitionTo(mConnected);
+                            break;
+                        case StackEvent.CONNECTION_STATE_DISCONNECTED:
+                            transitionTo(mDisconnected);
+                            break;
+                    }
+            }
+        }
         @Override
         public void exit() {
             removeMessages(CONNECT_TIMEOUT);
         }
 
-        // in Pending state
-        private void processConnectionEvent(BluetoothDevice device, int state) {
-            log("processConnectionEvent state " + state);
-            log("Devices curr: " + mCurrentDevice + " target: " + mTargetDevice + " incoming: "
-                    + mIncomingDevice + " device: " + device);
-            switch (state) {
-                case CONNECTION_STATE_DISCONNECTED:
-                    mAudioConfigs.remove(device);
-                    if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) {
-                        broadcastConnectionState(mCurrentDevice,
-                                BluetoothProfile.STATE_DISCONNECTED,
-                                BluetoothProfile.STATE_DISCONNECTING);
-                        synchronized (A2dpSinkStateMachine.this) {
-                            mCurrentDevice = null;
-                        }
-
-                        if (mTargetDevice != null) {
-                            if (!connectA2dpNative(getByteAddress(mTargetDevice))) {
-                                broadcastConnectionState(mTargetDevice,
-                                        BluetoothProfile.STATE_DISCONNECTED,
-                                        BluetoothProfile.STATE_CONNECTING);
-                                synchronized (A2dpSinkStateMachine.this) {
-                                    mTargetDevice = null;
-                                    transitionTo(mDisconnected);
-                                }
-                            }
-                        } else {
-                            synchronized (A2dpSinkStateMachine.this) {
-                                mIncomingDevice = null;
-                                transitionTo(mDisconnected);
-                            }
-                        }
-                    } else if (mTargetDevice != null && mTargetDevice.equals(device)) {
-                        // outgoing connection failed
-                        broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_DISCONNECTED,
-                                BluetoothProfile.STATE_CONNECTING);
-                        synchronized (A2dpSinkStateMachine.this) {
-                            mTargetDevice = null;
-                            transitionTo(mDisconnected);
-                        }
-                    } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) {
-                        broadcastConnectionState(mIncomingDevice,
-                                BluetoothProfile.STATE_DISCONNECTED,
-                                BluetoothProfile.STATE_CONNECTING);
-                        synchronized (A2dpSinkStateMachine.this) {
-                            mIncomingDevice = null;
-                            transitionTo(mDisconnected);
-                        }
-                    } else {
-                        loge("Unknown device Disconnected: " + device);
-                    }
-                    break;
-                case CONNECTION_STATE_CONNECTED:
-                    if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) {
-                        loge("current device is not null");
-                        // disconnection failed
-                        broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_CONNECTED,
-                                BluetoothProfile.STATE_DISCONNECTING);
-                        if (mTargetDevice != null) {
-                            broadcastConnectionState(mTargetDevice,
-                                    BluetoothProfile.STATE_DISCONNECTED,
-                                    BluetoothProfile.STATE_CONNECTING);
-                        }
-                        synchronized (A2dpSinkStateMachine.this) {
-                            mTargetDevice = null;
-                            transitionTo(mConnected);
-                        }
-                    } else if (mTargetDevice != null && mTargetDevice.equals(device)) {
-                        loge("target device is not null");
-                        broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_CONNECTED,
-                                BluetoothProfile.STATE_CONNECTING);
-                        synchronized (A2dpSinkStateMachine.this) {
-                            mCurrentDevice = mTargetDevice;
-                            mTargetDevice = null;
-                            transitionTo(mConnected);
-                        }
-                    } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) {
-                        loge("incoming device is not null");
-                        broadcastConnectionState(mIncomingDevice, BluetoothProfile.STATE_CONNECTED,
-                                BluetoothProfile.STATE_CONNECTING);
-                        synchronized (A2dpSinkStateMachine.this) {
-                            mCurrentDevice = mIncomingDevice;
-                            mIncomingDevice = null;
-                            transitionTo(mConnected);
-                        }
-                    } else {
-                        loge("Unknown device Connected: " + device);
-                        // something is wrong here, but sync our state with stack by connecting to
-                        // the new device and disconnect from previous device.
-                        broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED,
-                                BluetoothProfile.STATE_DISCONNECTED);
-                        broadcastConnectionState(mCurrentDevice,
-                                BluetoothProfile.STATE_DISCONNECTED,
-                                BluetoothProfile.STATE_CONNECTING);
-                        synchronized (A2dpSinkStateMachine.this) {
-                            mCurrentDevice = device;
-                            mTargetDevice = null;
-                            mIncomingDevice = null;
-                            transitionTo(mConnected);
-                        }
-                    }
-                    break;
-                case CONNECTION_STATE_CONNECTING:
-                    if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) {
-                        log("current device tries to connect back");
-                        // TODO(BT) ignore or reject
-                    } else if (mTargetDevice != null && mTargetDevice.equals(device)) {
-                        // The stack is connecting to target device or
-                        // there is an incoming connection from the target device at the same time
-                        // we already broadcasted the intent, doing nothing here
-                        log("Stack and target device are connecting");
-                    } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) {
-                        loge("Another connecting event on the incoming device");
-                    } else {
-                        // We get an incoming connecting request while Pending
-                        // TODO(BT) is stack handing this case? let's ignore it for now
-                        log("Incoming connection while pending, ignore");
-                    }
-                    break;
-                case CONNECTION_STATE_DISCONNECTING:
-                    if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) {
-                        // we already broadcasted the intent, doing nothing here
-                        if (DBG) {
-                            log("stack is disconnecting mCurrentDevice");
-                        }
-                    } else if (mTargetDevice != null && mTargetDevice.equals(device)) {
-                        loge("TargetDevice is getting disconnected");
-                    } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) {
-                        loge("IncomingDevice is getting disconnected");
-                    } else {
-                        loge("Disconnecting unknown device: " + device);
-                    }
-                    break;
-                default:
-                    loge("Incorrect state: " + state);
-                    break;
-            }
-        }
-
     }
 
-    private class Connected extends State {
+    class Connected extends State {
         @Override
         public void enter() {
-            log("Enter Connected: " + getCurrentMessage().what);
-            // Upon connected, the audio starts out as stopped
-            broadcastAudioState(mCurrentDevice, BluetoothA2dpSink.STATE_NOT_PLAYING,
-                    BluetoothA2dpSink.STATE_PLAYING);
-            synchronized (A2dpSinkStateMachine.this) {
-                if (mStreaming == null) {
-                    if (DBG) {
-                        log("Creating New A2dpSinkStreamHandler");
-                    }
-                    mStreaming = new A2dpSinkStreamHandler(A2dpSinkStateMachine.this, mContext);
-                }
-            }
-            if (mStreaming.getAudioFocus() == AudioManager.AUDIOFOCUS_NONE) {
-                informAudioFocusStateNative(0);
-            }
+            if (DBG) Log.d(TAG, "Enter Connected");
+            onConnectionStateChanged(BluetoothProfile.STATE_CONNECTED);
         }
 
         @Override
         public boolean processMessage(Message message) {
-            log("Connected process message: " + message.what);
-            if (mCurrentDevice == null) {
-                loge("ERROR: mCurrentDevice is null in Connected");
-                return NOT_HANDLED;
-            }
-
             switch (message.what) {
-                case CONNECT:
-                    logd("Disconnect before connecting to another target");
-                break;
-
-                case DISCONNECT: {
-                    BluetoothDevice device = (BluetoothDevice) message.obj;
-                    if (!mCurrentDevice.equals(device)) {
-                        break;
-                    }
-                    broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTING,
-                            BluetoothProfile.STATE_CONNECTED);
-                    if (!disconnectA2dpNative(getByteAddress(device))) {
-                        broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED,
-                                BluetoothProfile.STATE_DISCONNECTED);
-                        break;
-                    }
-                    mPlayingDevice = null;
-                    mStreaming.obtainMessage(A2dpSinkStreamHandler.DISCONNECT).sendToTarget();
-                    transitionTo(mPending);
-                }
-                break;
-
+                case DISCONNECT:
+                    transitionTo(mDisconnecting);
+                    mService.disconnectA2dpNative(mDeviceAddress);
+                    return true;
                 case STACK_EVENT:
-                    StackEvent event = (StackEvent) message.obj;
-                    switch (event.type) {
-                        case EVENT_TYPE_CONNECTION_STATE_CHANGED:
-                            processConnectionEvent(event.device, event.valueInt);
-                            break;
-                        case EVENT_TYPE_AUDIO_STATE_CHANGED:
-                            processAudioStateEvent(event.device, event.valueInt);
-                            break;
-                        case EVENT_TYPE_AUDIO_CONFIG_CHANGED:
-                            processAudioConfigEvent(event.device, event.audioConfig);
-                            break;
-                        default:
-                            loge("Unexpected stack event: " + event.type);
-                            break;
-                    }
-                    break;
-
-                case EVENT_AVRCP_CT_PLAY:
-                    mStreaming.obtainMessage(A2dpSinkStreamHandler.SNK_PLAY).sendToTarget();
-                    break;
-
-                case EVENT_AVRCP_TG_PLAY:
-                    mStreaming.obtainMessage(A2dpSinkStreamHandler.SRC_PLAY).sendToTarget();
-                    break;
-
-                case EVENT_AVRCP_CT_PAUSE:
-                    mStreaming.obtainMessage(A2dpSinkStreamHandler.SNK_PAUSE).sendToTarget();
-                    break;
-
-                case EVENT_AVRCP_TG_PAUSE:
-                    mStreaming.obtainMessage(A2dpSinkStreamHandler.SRC_PAUSE).sendToTarget();
-                    break;
-
-                case EVENT_REQUEST_FOCUS:
-                    mStreaming.obtainMessage(A2dpSinkStreamHandler.REQUEST_FOCUS).sendToTarget();
-                    break;
-
-                default:
-                    return NOT_HANDLED;
+                    processStackEvent((StackEvent) message.obj);
+                    return true;
             }
-            return HANDLED;
-        }
-
-        // in Connected state
-        private void processConnectionEvent(BluetoothDevice device, int state) {
-            switch (state) {
-                case CONNECTION_STATE_DISCONNECTED:
-                    mAudioConfigs.remove(device);
-                    if ((mPlayingDevice != null) && (device.equals(mPlayingDevice))) {
-                        mPlayingDevice = null;
-                    }
-                    if (mCurrentDevice.equals(device)) {
-                        broadcastConnectionState(mCurrentDevice,
-                                BluetoothProfile.STATE_DISCONNECTED,
-                                BluetoothProfile.STATE_CONNECTED);
-                        synchronized (A2dpSinkStateMachine.this) {
-                            // Take care of existing audio focus in the streaming state machine.
-                            mStreaming.obtainMessage(A2dpSinkStreamHandler.DISCONNECT)
-                                    .sendToTarget();
-                            mCurrentDevice = null;
-                            transitionTo(mDisconnected);
-                        }
-                    } else {
-                        loge("Disconnected from unknown device: " + device);
-                    }
-                    break;
-                default:
-                    loge("Connection State Device: " + device + " bad state: " + state);
-                    break;
-            }
-        }
-
-        private void processAudioStateEvent(BluetoothDevice device, int state) {
-            if (!mCurrentDevice.equals(device)) {
-                loge("Audio State Device:" + device + "is different from ConnectedDevice:"
-                        + mCurrentDevice);
-                return;
-            }
-            log(" processAudioStateEvent in state " + state);
-            switch (state) {
-                case AUDIO_STATE_STARTED:
-                    mStreaming.obtainMessage(A2dpSinkStreamHandler.SRC_STR_START).sendToTarget();
-                    if (mPlayingDevice == null) {
-                        mPlayingDevice = device;
-                        broadcastAudioState(device, BluetoothA2dpSink.STATE_PLAYING,
-                                BluetoothA2dpSink.STATE_NOT_PLAYING);
-                    }
-                    break;
-                case AUDIO_STATE_REMOTE_SUSPEND:
-                case AUDIO_STATE_STOPPED:
-                    mStreaming.obtainMessage(A2dpSinkStreamHandler.SRC_STR_STOP).sendToTarget();
-                    if (mPlayingDevice != null) {
-                        broadcastAudioState(device, BluetoothA2dpSink.STATE_NOT_PLAYING,
-                                BluetoothA2dpSink.STATE_PLAYING);
-                        mPlayingDevice = null;
-                    }
-                    break;
-                default:
-                    loge("Audio State Device: " + device + " bad state: " + state);
-                    break;
-            }
-        }
-    }
-
-    private void processAudioConfigEvent(BluetoothDevice device, BluetoothAudioConfig audioConfig) {
-        log("processAudioConfigEvent: " + device);
-        mAudioConfigs.put(device, audioConfig);
-        broadcastAudioConfig(device, audioConfig);
-    }
-
-    int getConnectionState(BluetoothDevice device) {
-        if (getCurrentState() == mDisconnected) {
-            return BluetoothProfile.STATE_DISCONNECTED;
-        }
-
-        synchronized (this) {
-            IState currentState = getCurrentState();
-            if (currentState == mPending) {
-                if ((mTargetDevice != null) && mTargetDevice.equals(device)) {
-                    return BluetoothProfile.STATE_CONNECTING;
-                }
-                if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) {
-                    return BluetoothProfile.STATE_DISCONNECTING;
-                }
-                if ((mIncomingDevice != null) && mIncomingDevice.equals(device)) {
-                    return BluetoothProfile.STATE_CONNECTING; // incoming connection
-                }
-                return BluetoothProfile.STATE_DISCONNECTED;
-            }
-
-            if (currentState == mConnected) {
-                if (mCurrentDevice != null && mCurrentDevice.equals(device)) {
-                    return BluetoothProfile.STATE_CONNECTED;
-                }
-                return BluetoothProfile.STATE_DISCONNECTED;
-            } else {
-                loge("Bad currentState: " + currentState);
-                return BluetoothProfile.STATE_DISCONNECTED;
-            }
-        }
-    }
-
-    BluetoothAudioConfig getAudioConfig(BluetoothDevice device) {
-        return mAudioConfigs.get(device);
-    }
-
-    List<BluetoothDevice> getConnectedDevices() {
-        List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
-        synchronized (this) {
-            if (getCurrentState() == mConnected) {
-                devices.add(mCurrentDevice);
-            }
-        }
-        return devices;
-    }
-
-    boolean isPlaying(BluetoothDevice device) {
-        synchronized (this) {
-            if ((mPlayingDevice != null) && (device.equals(mPlayingDevice))) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    // Utility Functions
-    boolean okToConnect(BluetoothDevice device) {
-        AdapterService adapterService = AdapterService.getAdapterService();
-        int priority = mService.getPriority(device);
-
-        // check priority and accept or reject the connection. if priority is undefined
-        // it is likely that our SDP has not completed and peer is initiating the
-        // connection. Allow this connection, provided the device is bonded
-        if ((BluetoothProfile.PRIORITY_OFF < priority) || (
-                (BluetoothProfile.PRIORITY_UNDEFINED == priority) && (device.getBondState()
-                        != BluetoothDevice.BOND_NONE))) {
-            return true;
-        }
-        logw("okToConnect not OK to connect " + device);
-        return false;
-    }
-
-    synchronized List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
-        List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>();
-        Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices();
-        int connectionState;
-
-        for (BluetoothDevice device : bondedDevices) {
-            ParcelUuid[] featureUuids = device.getUuids();
-            if (!BluetoothUuid.isUuidPresent(featureUuids, BluetoothUuid.AudioSource)) {
-                continue;
-            }
-            connectionState = getConnectionState(device);
-            for (int i = 0; i < states.length; i++) {
-                if (connectionState == states[i]) {
-                    deviceList.add(device);
-                }
-            }
-        }
-        return deviceList;
-    }
-
-
-    // This method does not check for error conditon (newState == prevState)
-    private void broadcastConnectionState(BluetoothDevice device, int newState, int prevState) {
-
-        int delay = 0;
-        mIntentBroadcastHandler.sendMessageDelayed(
-                mIntentBroadcastHandler.obtainMessage(MSG_CONNECTION_STATE_CHANGED, prevState,
-                        newState, device), delay);
-    }
-
-    private void broadcastAudioState(BluetoothDevice device, int state, int prevState) {
-        Intent intent = new Intent(BluetoothA2dpSink.ACTION_PLAYING_STATE_CHANGED);
-        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
-        intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
-        intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
-//FIXME        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
-        mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
-
-        log("A2DP Playing state : device: " + device + " State:" + prevState + "->" + state);
-    }
-
-    private void broadcastAudioConfig(BluetoothDevice device, BluetoothAudioConfig audioConfig) {
-        Intent intent = new Intent(BluetoothA2dpSink.ACTION_AUDIO_CONFIG_CHANGED);
-        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
-        intent.putExtra(BluetoothA2dpSink.EXTRA_AUDIO_CONFIG, audioConfig);
-//FIXME        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
-        mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
-
-        log("A2DP Audio Config : device: " + device + " config: " + audioConfig);
-    }
-
-    private byte[] getByteAddress(BluetoothDevice device) {
-        return Utils.getBytesFromAddress(device.getAddress());
-    }
-
-    private void onConnectionStateChanged(byte[] address, int state) {
-        StackEvent event = new StackEvent(EVENT_TYPE_CONNECTION_STATE_CHANGED);
-        event.device = getDevice(address);
-        event.valueInt = state;
-        sendMessage(STACK_EVENT, event);
-    }
-
-    private void onAudioStateChanged(byte[] address, int state) {
-        StackEvent event = new StackEvent(EVENT_TYPE_AUDIO_STATE_CHANGED);
-        event.device = getDevice(address);
-        event.valueInt = state;
-        sendMessage(STACK_EVENT, event);
-    }
-
-    private void onAudioConfigChanged(byte[] address, int sampleRate, int channelCount) {
-        StackEvent event = new StackEvent(EVENT_TYPE_AUDIO_CONFIG_CHANGED);
-        event.device = getDevice(address);
-        int channelConfig =
-                (channelCount == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO);
-        event.audioConfig =
-                new BluetoothAudioConfig(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);
-        sendMessage(STACK_EVENT, event);
-    }
-
-    private BluetoothDevice getDevice(byte[] address) {
-        return mAdapter.getRemoteDevice(Utils.getAddressStringFromByte(address));
-    }
-
-    private class StackEvent {
-        public int type = EVENT_TYPE_NONE;
-        public BluetoothDevice device = null;
-        public int valueInt = 0;
-        public BluetoothAudioConfig audioConfig = null;
-
-        private StackEvent(int type) {
-            this.type = type;
-        }
-    }
-
-    /** Handles A2DP connection state change intent broadcasts. */
-    private class IntentBroadcastHandler extends Handler {
-
-        private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) {
-            if (prevState != state && state == BluetoothProfile.STATE_CONNECTED) {
-                MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.A2DP_SINK);
-            }
-            Intent intent = new Intent(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
-            intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
-            intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
-            intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
-//FIXME            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
-            mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
-            log("Connection state " + device + ": " + prevState + "->" + state);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_CONNECTION_STATE_CHANGED:
-                    onConnectionStateChanged((BluetoothDevice) msg.obj, msg.arg1, msg.arg2);
-                    break;
-            }
-        }
-    }
-
-    public boolean sendPassThruPlay(BluetoothDevice mDevice) {
-        log("sendPassThruPlay + ");
-        AvrcpControllerService avrcpCtrlService =
-                AvrcpControllerService.getAvrcpControllerService();
-        if ((avrcpCtrlService != null) && (mDevice != null)
-                && (avrcpCtrlService.getConnectedDevices().contains(mDevice))) {
-            avrcpCtrlService.sendPassThroughCmd(mDevice,
-                    AvrcpControllerService.PASS_THRU_CMD_ID_PLAY,
-                    AvrcpControllerService.KEY_STATE_PRESSED);
-            avrcpCtrlService.sendPassThroughCmd(mDevice,
-                    AvrcpControllerService.PASS_THRU_CMD_ID_PLAY,
-                    AvrcpControllerService.KEY_STATE_RELEASED);
-            log(" sendPassThruPlay command sent - ");
-            return true;
-        } else {
-            log("passthru command not sent, connection unavailable");
             return false;
         }
+
+        void processStackEvent(StackEvent event) {
+            switch (event.mType) {
+                case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
+                    switch (event.mState) {
+                        case StackEvent.CONNECTION_STATE_DISCONNECTING:
+                            transitionTo(mDisconnecting);
+                            break;
+                        case StackEvent.CONNECTION_STATE_DISCONNECTED:
+                            transitionTo(mDisconnected);
+                            break;
+                    }
+                    break;
+                case StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED:
+                    mAudioConfig = new BluetoothAudioConfig(event.mSampleRate, event.mChannelCount,
+                            AudioFormat.ENCODING_PCM_16BIT);
+                    break;
+            }
+        }
     }
 
-    // Event types for STACK_EVENT message
-    private static final int EVENT_TYPE_NONE = 0;
-    private static final int EVENT_TYPE_CONNECTION_STATE_CHANGED = 1;
-    private static final int EVENT_TYPE_AUDIO_STATE_CHANGED = 2;
-    private static final int EVENT_TYPE_AUDIO_CONFIG_CHANGED = 3;
+    protected class Disconnecting extends State {
+        @Override
+        public void enter() {
+            if (DBG) Log.d(TAG, "Enter Disconnecting");
+            onConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTING);
+            transitionTo(mDisconnected);
+        }
+    }
 
-    // Do not modify without updating the HAL bt_av.h files.
-
-    // match up with btav_connection_state_t enum of bt_av.h
-    static final int CONNECTION_STATE_DISCONNECTED = 0;
-    static final int CONNECTION_STATE_CONNECTING = 1;
-    static final int CONNECTION_STATE_CONNECTED = 2;
-    static final int CONNECTION_STATE_DISCONNECTING = 3;
-
-    // match up with btav_audio_state_t enum of bt_av.h
-    static final int AUDIO_STATE_REMOTE_SUSPEND = 0;
-    static final int AUDIO_STATE_STOPPED = 1;
-    static final int AUDIO_STATE_STARTED = 2;
-
-    private static native void classInitNative();
-
-    private native void initNative();
-
-    private native void cleanupNative();
-
-    private native boolean connectA2dpNative(byte[] address);
-
-    private native boolean disconnectA2dpNative(byte[] address);
-
-    public native void informAudioFocusStateNative(int focusGranted);
-
-    public native void informAudioTrackGainNative(float focusGranted);
+    protected void onConnectionStateChanged(int currentState) {
+        if (mMostRecentState == currentState) {
+            return;
+        }
+        if (currentState == BluetoothProfile.STATE_CONNECTED) {
+            MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.A2DP_SINK);
+        }
+        if (DBG) {
+            Log.d(TAG, "Connection state " + mDevice + ": " + mMostRecentState + "->"
+                    + currentState);
+        }
+        Intent intent = new Intent(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
+        intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, mMostRecentState);
+        intent.putExtra(BluetoothProfile.EXTRA_STATE, currentState);
+        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
+        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+        mMostRecentState = currentState;
+        mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
+    }
 }
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java
index 94658f6..d3dd731 100644
--- a/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java
@@ -23,12 +23,13 @@
 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.util.Log;
 
 import com.android.bluetooth.R;
-import com.android.bluetooth.avrcpcontroller.AvrcpControllerService;
+import com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService;
 import com.android.bluetooth.hfpclient.HeadsetClientService;
 
 import java.util.List;
@@ -52,8 +53,8 @@
  * restored.
  */
 public class A2dpSinkStreamHandler extends Handler {
-    private static final boolean DBG = false;
     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;
@@ -77,7 +78,7 @@
     private static final int STATE_FOCUS_GRANTED = 1;
 
     // Private variables.
-    private A2dpSinkStateMachine mA2dpSinkSm;
+    private A2dpSinkService mA2dpSinkService;
     private Context mContext;
     private AudioManager mAudioManager;
     // Keep track if the remote device is providing audio
@@ -86,6 +87,16 @@
     // 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
@@ -98,12 +109,26 @@
         }
     };
 
-    public A2dpSinkStreamHandler(A2dpSinkStateMachine a2dpSinkSm, Context context) {
-        mA2dpSinkSm = a2dpSinkSm;
+    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) {
@@ -112,13 +137,10 @@
         }
         switch (message.what) {
             case SRC_STR_START:
+                mStreamAvailable = true;
                 if (isTvDevice() || shouldRequestFocus()) {
                     requestAudioFocusIfNone();
                 }
-                // Audio stream has started, stop it if we don't have focus.
-                if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) {
-                    sendAvrcpPause();
-                }
                 break;
 
             case SRC_STR_STOP:
@@ -142,10 +164,6 @@
                     requestAudioFocusIfNone();
                     break;
                 }
-                // Otherwise, pause if we don't have focus
-                if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) {
-                    sendAvrcpPause();
-                }
                 break;
 
             case SRC_PAUSE:
@@ -223,12 +241,17 @@
      * Utility functions.
      */
     private void requestAudioFocusIfNone() {
+        if (DBG) Log.d(TAG, "requestAudioFocusIfNone()");
         if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) {
             requestAudioFocus();
         }
+        // On the off change mMediaPlayer errors out and dies, we want to make sure we retry this.
+        // This function immediately exits if we have a MediaPlayer object.
+        requestMediaKeyFocus();
     }
 
     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 =
@@ -252,89 +275,92 @@
         return focusRequestStatus;
     }
 
+    /**
+     * Creates a MediaPlayer that plays a silent audio sample so that MediaSessionService will be
+     * aware of the fact that Bluetooth is playing audio.
+     *
+     * 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) return;
+
+        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();
+        releaseMediaKeyFocus();
         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() {
-        mA2dpSinkSm.informAudioFocusStateNative(STATE_FOCUS_GRANTED);
-        mA2dpSinkSm.informAudioTrackGainNative(1.0f);
+        mA2dpSinkService.informAudioFocusStateNative(STATE_FOCUS_GRANTED);
+        mA2dpSinkService.informAudioTrackGainNative(1.0f);
     }
 
     private void stopFluorideStreaming() {
-        mA2dpSinkSm.informAudioFocusStateNative(STATE_FOCUS_LOST);
+        mA2dpSinkService.informAudioFocusStateNative(STATE_FOCUS_LOST);
     }
 
     private void setFluorideAudioTrackGain(float gain) {
-        mA2dpSinkSm.informAudioTrackGainNative(gain);
+        mA2dpSinkService.informAudioTrackGainNative(gain);
     }
 
     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) {
-            List<BluetoothDevice> connectedDevices = avrcpService.getConnectedDevices();
-            if (!connectedDevices.isEmpty()) {
-                BluetoothDevice targetDevice = connectedDevices.get(0);
-                if (DBG) {
-                    Log.d(TAG, "Pausing AVRCP.");
-                }
-                avrcpService.sendPassThroughCmd(targetDevice,
-                        AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE,
-                        AvrcpControllerService.KEY_STATE_PRESSED);
-                avrcpService.sendPassThroughCmd(targetDevice,
-                        AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE,
-                        AvrcpControllerService.KEY_STATE_RELEASED);
-            }
-        } else {
-            Log.e(TAG, "Passthrough not sent, connection un-available.");
-        }
+        BluetoothMediaBrowserService.pause();
     }
 
     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) {
-            List<BluetoothDevice> connectedDevices = avrcpService.getConnectedDevices();
-            if (!connectedDevices.isEmpty()) {
-                BluetoothDevice targetDevice = connectedDevices.get(0);
-                if (DBG) {
-                    Log.d(TAG, "Playing AVRCP.");
-                }
-                avrcpService.sendPassThroughCmd(targetDevice,
-                        AvrcpControllerService.PASS_THRU_CMD_ID_PLAY,
-                        AvrcpControllerService.KEY_STATE_PRESSED);
-                avrcpService.sendPassThroughCmd(targetDevice,
-                        AvrcpControllerService.PASS_THRU_CMD_ID_PLAY,
-                        AvrcpControllerService.KEY_STATE_RELEASED);
-            }
-        } else {
-            Log.e(TAG, "Passthrough not sent, connection un-available.");
-        }
+        BluetoothMediaBrowserService.play();
     }
 
     private boolean inCallFromStreamingDevice() {
-        AvrcpControllerService avrcpService = AvrcpControllerService.getAvrcpControllerService();
         BluetoothDevice targetDevice = null;
-        if (avrcpService != null) {
-            List<BluetoothDevice> connectedDevices = avrcpService.getConnectedDevices();
-            if (!connectedDevices.isEmpty()) {
-                targetDevice = connectedDevices.get(0);
-            }
+        List<BluetoothDevice> connectedDevices = mA2dpSinkService.getConnectedDevices();
+        if (!connectedDevices.isEmpty()) {
+            targetDevice = connectedDevices.get(0);
         }
         HeadsetClientService headsetClientService = HeadsetClientService.getHeadsetClientService();
-        if (targetDevice != null  && headsetClientService != null) {
+        if (targetDevice != null && headsetClientService != null) {
             return headsetClientService.getCurrentCalls(targetDevice).size() > 0;
         }
         return false;
@@ -354,7 +380,7 @@
 
     private boolean shouldRequestFocus() {
         return mContext.getResources()
-            .getBoolean(R.bool.a2dp_sink_automatically_request_audio_focus);
+                .getBoolean(R.bool.a2dp_sink_automatically_request_audio_focus);
     }
 
 }
diff --git a/src/com/android/bluetooth/a2dpsink/StackEvent.java b/src/com/android/bluetooth/a2dpsink/StackEvent.java
new file mode 100644
index 0000000..68a6ce9
--- /dev/null
+++ b/src/com/android/bluetooth/a2dpsink/StackEvent.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2018 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;
+
+final class StackEvent {
+    // Event types for STACK_EVENT message
+    static final int EVENT_TYPE_NONE = 0;
+    static final int EVENT_TYPE_CONNECTION_STATE_CHANGED = 1;
+    static final int EVENT_TYPE_AUDIO_STATE_CHANGED = 2;
+    static final int EVENT_TYPE_AUDIO_CONFIG_CHANGED = 3;
+
+    // match up with btav_connection_state_t enum of bt_av.h
+    static final int CONNECTION_STATE_DISCONNECTED = 0;
+    static final int CONNECTION_STATE_CONNECTING = 1;
+    static final int CONNECTION_STATE_CONNECTED = 2;
+    static final int CONNECTION_STATE_DISCONNECTING = 3;
+
+    // match up with btav_audio_state_t enum of bt_av.h
+    static final int AUDIO_STATE_REMOTE_SUSPEND = 0;
+    static final int AUDIO_STATE_STOPPED = 1;
+    static final int AUDIO_STATE_STARTED = 2;
+
+    int mType = EVENT_TYPE_NONE;
+    BluetoothDevice mDevice = null;
+    int mState = 0;
+    int mSampleRate = 0;
+    int mChannelCount = 0;
+
+    private StackEvent(int type) {
+        this.mType = type;
+    }
+
+    @Override
+    public String toString() {
+        switch (mType) {
+            case EVENT_TYPE_CONNECTION_STATE_CHANGED:
+                return "EVENT_TYPE_CONNECTION_STATE_CHANGED " + mState;
+            case EVENT_TYPE_AUDIO_STATE_CHANGED:
+                return "EVENT_TYPE_AUDIO_STATE_CHANGED " + mState;
+            case EVENT_TYPE_AUDIO_CONFIG_CHANGED:
+                return "EVENT_TYPE_AUDIO_CONFIG_CHANGED " + mSampleRate + ":" + mChannelCount;
+            default:
+                return "Unknown";
+        }
+    }
+
+    static StackEvent connectionStateChanged(BluetoothDevice device, int state) {
+        StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+        event.mDevice = device;
+        event.mState = state;
+        return event;
+    }
+
+    static StackEvent audioStateChanged(BluetoothDevice device, int state) {
+        StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED);
+        event.mDevice = device;
+        event.mState = state;
+        return event;
+    }
+
+    static StackEvent audioConfigChanged(BluetoothDevice device, int sampleRate,
+            int channelCount) {
+        StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED);
+        event.mDevice = device;
+        event.mSampleRate = sampleRate;
+        event.mChannelCount = channelCount;
+        return event;
+    }
+}
diff --git a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java
index f6ce92f..52178f1 100644
--- a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java
+++ b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java
@@ -21,14 +21,11 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.IBluetoothAvrcpController;
+import android.content.Intent;
 import android.media.MediaDescription;
-import android.media.MediaMetadata;
-import android.media.browse.MediaBrowser;
 import android.media.browse.MediaBrowser.MediaItem;
 import android.media.session.PlaybackState;
 import android.os.Bundle;
-import android.os.HandlerThread;
-import android.os.Message;
 import android.util.Log;
 
 import com.android.bluetooth.Utils;
@@ -37,15 +34,21 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * Provides Bluetooth AVRCP Controller profile, as a service in the Bluetooth application.
  */
 public class AvrcpControllerService extends ProfileService {
     static final String TAG = "AvrcpControllerService";
-    static final boolean DBG = false;
-    static final boolean VDBG = false;
+    static final int MAXIMUM_CONNECTED_DEVICES = 5;
+    static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+    static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
+
+    public static final String MEDIA_ITEM_UID_KEY = "media-item-uid-key";
     /*
      *  Play State Values from JNI
      */
@@ -56,97 +59,19 @@
     private static final byte JNI_PLAY_STATUS_REV_SEEK = 0x04;
     private static final byte JNI_PLAY_STATUS_ERROR = -1;
 
-    /*
-     * Browsing Media Item Attribute IDs
-     * This should be kept in sync with BTRC_MEDIA_ATTR_ID_* in bt_rc.h
+    /* Folder/Media Item scopes.
+     * Keep in sync with AVRCP 1.6 sec. 6.10.1
      */
-    private static final int JNI_MEDIA_ATTR_ID_INVALID = -1;
-    private static final int JNI_MEDIA_ATTR_ID_TITLE = 0x00000001;
-    private static final int JNI_MEDIA_ATTR_ID_ARTIST = 0x00000002;
-    private static final int JNI_MEDIA_ATTR_ID_ALBUM = 0x00000003;
-    private static final int JNI_MEDIA_ATTR_ID_TRACK_NUM = 0x00000004;
-    private static final int JNI_MEDIA_ATTR_ID_NUM_TRACKS = 0x00000005;
-    private static final int JNI_MEDIA_ATTR_ID_GENRE = 0x00000006;
-    private static final int JNI_MEDIA_ATTR_ID_PLAYING_TIME = 0x00000007;
+    public static final byte BROWSE_SCOPE_PLAYER_LIST = 0x00;
+    public static final byte BROWSE_SCOPE_VFS = 0x01;
+    public static final byte BROWSE_SCOPE_SEARCH = 0x02;
+    public static final byte BROWSE_SCOPE_NOW_PLAYING = 0x03;
 
-    /*
-     * Browsing folder types
-     * This should be kept in sync with BTRC_FOLDER_TYPE_* in bt_rc.h
+    /* Folder navigation directions
+     * This is borrowed from AVRCP 1.6 spec and must be kept with same values
      */
-    private static final int JNI_FOLDER_TYPE_TITLES = 0x01;
-    private static final int JNI_FOLDER_TYPE_ALBUMS = 0x02;
-    private static final int JNI_FOLDER_TYPE_ARTISTS = 0x03;
-    private static final int JNI_FOLDER_TYPE_GENRES = 0x04;
-    private static final int JNI_FOLDER_TYPE_PLAYLISTS = 0x05;
-    private static final int JNI_FOLDER_TYPE_YEARS = 0x06;
-
-    /*
-     * AVRCP Error types as defined in spec. Also they should be in sync with btrc_status_t.
-     * NOTE: Not all may be defined.
-     */
-    private static final int JNI_AVRC_STS_NO_ERROR = 0x04;
-    private static final int JNI_AVRC_INV_RANGE = 0x0b;
-
-    /**
-     * Intent used to broadcast the change in browse connection state of the AVRCP Controller
-     * profile.
-     *
-     * <p>This intent will have 2 extras:
-     * <ul>
-     *   <li> {@link BluetoothProfile#EXTRA_STATE} - The current state of the profile. </li>
-     *   <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
-     * </ul>
-     *
-     * <p>{@link #EXTRA_STATE} can be any of
-     * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
-     * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
-     *
-     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to
-     * receive.
-     */
-    public static final String ACTION_BROWSE_CONNECTION_STATE_CHANGED =
-            "android.bluetooth.avrcp-controller.profile.action.BROWSE_CONNECTION_STATE_CHANGED";
-
-    /**
-     * intent used to broadcast the change in metadata state of playing track on the avrcp
-     * ag.
-     *
-     * <p>this intent will have the two extras:
-     * <ul>
-     *    <li> {@link #extra_metadata} - {@link mediametadata} containing the current metadata.</li>
-     *    <li> {@link #extra_playback} - {@link playbackstate} containing the current playback
-     *    state. </li>
-     * </ul>
-     */
-    public static final String ACTION_TRACK_EVENT =
-            "android.bluetooth.avrcp-controller.profile.action.TRACK_EVENT";
-
-    /**
-     * Intent used to broadcast the change of folder list.
-     *
-     * <p>This intent will have the one extra:
-     * <ul>
-     *    <li> {@link #EXTRA_FOLDER_LIST} - array of {@link MediaBrowser#MediaItem}
-     *    containing the folder listing of currently selected folder.
-     * </ul>
-     */
-    public static final String ACTION_FOLDER_LIST =
-            "android.bluetooth.avrcp-controller.profile.action.FOLDER_LIST";
-
-    public static final String EXTRA_FOLDER_LIST =
-            "android.bluetooth.avrcp-controller.profile.extra.FOLDER_LIST";
-
-    public static final String EXTRA_FOLDER_ID = "com.android.bluetooth.avrcp.EXTRA_FOLDER_ID";
-    public static final String EXTRA_FOLDER_BT_ID =
-            "com.android.bluetooth.avrcp-controller.EXTRA_FOLDER_BT_ID";
-
-    public static final String EXTRA_METADATA =
-            "android.bluetooth.avrcp-controller.profile.extra.METADATA";
-
-    public static final String EXTRA_PLAYBACK =
-            "android.bluetooth.avrcp-controller.profile.extra.PLAYBACK";
-
-    public static final String MEDIA_ITEM_UID_KEY = "media-item-uid-key";
+    public static final byte FOLDER_NAVIGATION_DIRECTION_UP = 0x00;
+    public static final byte FOLDER_NAVIGATION_DIRECTION_DOWN = 0x01;
 
     /*
      * KeyCoded for Pass Through Commands
@@ -165,293 +90,109 @@
     public static final int KEY_STATE_PRESSED = 0;
     public static final int KEY_STATE_RELEASED = 1;
 
-    /* Group Navigation Key Codes */
-    public static final int PASS_THRU_CMD_ID_NEXT_GRP = 0x00;
-    public static final int PASS_THRU_CMD_ID_PREV_GRP = 0x01;
+    static BrowseTree sBrowseTree;
+    private static AvrcpControllerService sService;
+    private final BluetoothAdapter mAdapter;
 
-    /* Folder navigation directions
-     * This is borrowed from AVRCP 1.6 spec and must be kept with same values
-     */
-    public static final byte FOLDER_NAVIGATION_DIRECTION_UP = 0x00;
-    public static final byte FOLDER_NAVIGATION_DIRECTION_DOWN = 0x01;
-
-    /* Folder/Media Item scopes.
-     * Keep in sync with AVRCP 1.6 sec. 6.10.1
-     */
-    public static final int BROWSE_SCOPE_PLAYER_LIST = 0x00;
-    public static final int BROWSE_SCOPE_VFS = 0x01;
-    public static final int BROWSE_SCOPE_SEARCH = 0x02;
-    public static final int BROWSE_SCOPE_NOW_PLAYING = 0x03;
-
-    private AvrcpControllerStateMachine mAvrcpCtSm;
-    private static AvrcpControllerService sAvrcpControllerService;
-    // UID size is 8 bytes (AVRCP 1.6 spec)
-    private static final byte[] EMPTY_UID = {0, 0, 0, 0, 0, 0, 0, 0};
-
-    // We only support one device.
-    private BluetoothDevice mConnectedDevice = null;
-    // If browse is supported (only valid if mConnectedDevice != null).
-    private boolean mBrowseConnected = false;
-    // Caches the current browse folder. If this is null then root is the currently browsed folder
-    // (which also has no UID).
-    private String mCurrentBrowseFolderUID = null;
+    protected Map<BluetoothDevice, AvrcpControllerStateMachine> mDeviceStateMap =
+            new ConcurrentHashMap<>(1);
 
     static {
         classInitNative();
     }
 
     public AvrcpControllerService() {
-        initNative();
-    }
-
-    @Override
-    protected IProfileServiceBinder initBinder() {
-        return new BluetoothAvrcpControllerBinder(this);
+        super();
+        mAdapter = BluetoothAdapter.getDefaultAdapter();
     }
 
     @Override
     protected boolean start() {
-        HandlerThread thread = new HandlerThread("BluetoothAvrcpHandler");
-        thread.start();
-        mAvrcpCtSm = new AvrcpControllerStateMachine(this);
-        mAvrcpCtSm.start();
+        initNative();
+        sBrowseTree = new BrowseTree(null);
+        sService = this;
 
-        setAvrcpControllerService(this);
+        // Start the media browser service.
+        Intent startIntent = new Intent(this, BluetoothMediaBrowserService.class);
+        startService(startIntent);
         return true;
     }
 
     @Override
     protected boolean stop() {
-        setAvrcpControllerService(null);
-        if (mAvrcpCtSm != null) {
-            mAvrcpCtSm.doQuit();
+        Intent stopIntent = new Intent(this, BluetoothMediaBrowserService.class);
+        stopService(stopIntent);
+        for (AvrcpControllerStateMachine stateMachine : mDeviceStateMap.values()) {
+            stateMachine.quitNow();
         }
+
+        sService = null;
+        sBrowseTree = null;
         return true;
     }
 
-    //API Methods
-
-    public static synchronized AvrcpControllerService getAvrcpControllerService() {
-        if (sAvrcpControllerService == null) {
-            Log.w(TAG, "getAvrcpControllerService(): service is null");
-            return null;
-        }
-        if (!sAvrcpControllerService.isAvailable()) {
-            Log.w(TAG, "getAvrcpControllerService(): service is not available ");
-            return null;
-        }
-        return sAvrcpControllerService;
+    public static AvrcpControllerService getAvrcpControllerService() {
+        return sService;
     }
 
-    private static synchronized void setAvrcpControllerService(AvrcpControllerService instance) {
-        if (DBG) {
-            Log.d(TAG, "setAvrcpControllerService(): set to: " + instance);
-        }
-        sAvrcpControllerService = instance;
+    protected AvrcpControllerStateMachine newStateMachine(BluetoothDevice device) {
+        return new AvrcpControllerStateMachine(device, this);
     }
 
-    public synchronized List<BluetoothDevice> getConnectedDevices() {
-        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-        List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
-        if (mConnectedDevice != null) {
-            devices.add(mConnectedDevice);
+    private void refreshContents(BrowseTree.BrowseNode node) {
+        if (node.mDevice == null) {
+            return;
         }
-        return devices;
+        AvrcpControllerStateMachine stateMachine = getStateMachine(node.mDevice);
+        if (stateMachine != null) {
+            stateMachine.requestContents(node);
+        }
     }
 
+    /*Java API*/
+
     /**
-     * This function only supports STATE_CONNECTED
+     * Get a List of MediaItems that are children of the specified media Id
+     *
+     * @param parentMediaId The player or folder to get the contents of
+     * @return List of Children if available, an empty list if there are none,
+     * or null if a search must be performed.
      */
-    public synchronized List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
-        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-        List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
-        for (int i = 0; i < states.length; i++) {
-            if (states[i] == BluetoothProfile.STATE_CONNECTED && mConnectedDevice != null) {
-                devices.add(mConnectedDevice);
+    public synchronized List<MediaItem> getContents(String parentMediaId) {
+        if (DBG) Log.d(TAG, "getContents(" + parentMediaId + ")");
+
+        BrowseTree.BrowseNode requestedNode = sBrowseTree.findBrowseNodeByID(parentMediaId);
+        if (requestedNode == null) {
+            for (AvrcpControllerStateMachine stateMachine : mDeviceStateMap.values()) {
+                requestedNode = stateMachine.findNode(parentMediaId);
+                if (requestedNode != null) {
+                    Log.d(TAG, "Found a node");
+                    break;
+                }
             }
         }
-        return devices;
-    }
 
-    public synchronized int getConnectionState(BluetoothDevice device) {
-        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-        return (mConnectedDevice != null ? BluetoothProfile.STATE_CONNECTED
-                : BluetoothProfile.STATE_DISCONNECTED);
-    }
-
-    public synchronized void sendGroupNavigationCmd(BluetoothDevice device, int keyCode,
-            int keyState) {
-        Log.v(TAG, "sendGroupNavigationCmd keyCode: " + keyCode + " keyState: " + keyState);
-        if (device == null) {
-            Log.e(TAG, "sendGroupNavigationCmd device is null");
-        }
-
-        if (!(device.equals(mConnectedDevice))) {
-            Log.e(TAG, " Device does not match " + device + " connected " + mConnectedDevice);
-            return;
-        }
-        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-        Message msg = mAvrcpCtSm.obtainMessage(
-                AvrcpControllerStateMachine.MESSAGE_SEND_GROUP_NAVIGATION_CMD,
-                keyCode, keyState, device);
-        mAvrcpCtSm.sendMessage(msg);
-    }
-
-    public synchronized void sendPassThroughCmd(BluetoothDevice device, int keyCode, int keyState) {
-        Log.v(TAG, "sendPassThroughCmd keyCode: " + keyCode + " keyState: " + keyState);
-        if (device == null) {
-            Log.e(TAG, "sendPassThroughCmd Device is null");
-            return;
-        }
-
-        if (!device.equals(mConnectedDevice)) {
-            Log.w(TAG, " Device does not match device " + device + " conn " + mConnectedDevice);
-            return;
-        }
-
-        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-        Message msg =
-                mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.MESSAGE_SEND_PASS_THROUGH_CMD,
-                        keyCode, keyState, device);
-        mAvrcpCtSm.sendMessage(msg);
-    }
-
-    public synchronized MediaMetadata getMetaData(BluetoothDevice device) {
-        if (DBG) {
-            Log.d(TAG, "getMetaData");
-        }
-        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-        if (device == null) {
-            Log.e(TAG, "getMetadata device is null");
+        if (requestedNode == null) {
+            if (DBG) Log.d(TAG, "Didn't find a node");
             return null;
+        } else {
+            if (!requestedNode.isCached()) {
+                if (DBG) Log.d(TAG, "node is not cached");
+                refreshContents(requestedNode);
+            }
+            if (DBG) Log.d(TAG, "Returning contents");
+            return requestedNode.getContents();
         }
-
-        if (!device.equals(mConnectedDevice)) {
-            return null;
-        }
-        return mAvrcpCtSm.getCurrentMetaData();
     }
 
-    public PlaybackState getPlaybackState(BluetoothDevice device) {
-        // Get the cached state by default.
-        return getPlaybackState(device, true);
-    }
-
-    // cached can be used to force a getPlaybackState command. Useful for PTS testing.
-    public synchronized PlaybackState getPlaybackState(BluetoothDevice device, boolean cached) {
-        if (DBG) {
-            Log.d(TAG, "getPlayBackState device = " + device);
-        }
-
-        if (device == null) {
-            Log.e(TAG, "getPlaybackState device is null");
-            return null;
-        }
-
-        if (!device.equals(mConnectedDevice)) {
-            Log.e(TAG, "Device " + device + " does not match connected deivce " + mConnectedDevice);
-            return null;
-
-        }
-        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-        return mAvrcpCtSm.getCurrentPlayBackState(cached);
-    }
-
-    public synchronized BluetoothAvrcpPlayerSettings getPlayerSettings(BluetoothDevice device) {
-        if (DBG) {
-            Log.d(TAG, "getPlayerApplicationSetting ");
-        }
-
-        if (device == null) {
-            Log.e(TAG, "getPlayerSettings device is null");
-            return null;
-        }
-
-        if (!device.equals(mConnectedDevice)) {
-            Log.e(TAG, "device " + device + " does not match connected device " + mConnectedDevice);
-            return null;
-        }
-
-        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-
-        /* Do nothing */
-        return null;
-    }
-
-    public boolean setPlayerApplicationSetting(BluetoothAvrcpPlayerSettings plAppSetting) {
-        if (DBG) {
-            Log.d(TAG, "getPlayerApplicationSetting");
-        }
-
-        /* Do nothing */
-        return false;
-    }
-
-    /**
-     * Retreive the contents of the directory from the associated bluetooth device.
-     */
-    public synchronized List<MediaItem> getContents(BluetoothDevice device, String parentMediaId) {
-        return mAvrcpCtSm.getContents(parentMediaId);
-    }
-/*
-    public synchronized boolean getFolderList(BluetoothDevice device, String id, int start,
-            int items) {
-        if (DBG) {
-            Log.d(TAG, "getFolderListing device = " + device + " start = " + start + "items = "
-                    + items);
-        }
-
-        if (device == null) {
-            Log.e(TAG, "getFolderListing device is null");
-            return false;
-        }
-
-        if (!device.equals(mConnectedDevice)) {
-            Log.e(TAG, "getFolderListing device " + device + " does not match " + mConnectedDevice);
-            return false;
-        }
-
-        if (!mBrowseConnected) {
-            Log.e(TAG, "getFolderListing browse not yet connected");
-            return false;
-        }
-
-        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-
-        Message msg =
-                mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.MESSAGE_GET_FOLDER_LIST, start,
-                        items, id);
-        mAvrcpCtSm.sendMessage(msg);
-        return true;
-    }
-*/
-    public synchronized void fetchAttrAndPlayItem(BluetoothDevice device, String uid) {
-        if (DBG) {
-            Log.d(TAG, "fetchAttrAndPlayItem device = " + device + " uid " + uid);
-        }
-
-        if (device == null) {
-            Log.e(TAG, "fetchAttrAndPlayItem device is null");
-            return;
-        }
-
-        if (!device.equals(mConnectedDevice)) {
-            Log.e(TAG, "fetchAttrAndPlayItem device " + device + " does not match "
-                    + mConnectedDevice);
-            return;
-        }
-
-        if (!mBrowseConnected) {
-            Log.e(TAG, "fetchAttrAndPlayItem browse not yet connected");
-            return;
-        }
-        mAvrcpCtSm.fetchAttrAndPlayItem(uid);
+    @Override
+    protected IProfileServiceBinder initBinder() {
+        return new AvrcpControllerServiceBinder(this);
     }
 
     //Binder object: Must be static class or memory leak may occur
-    private static class BluetoothAvrcpControllerBinder extends IBluetoothAvrcpController.Stub
+    private static class AvrcpControllerServiceBinder extends IBluetoothAvrcpController.Stub
             implements IProfileServiceBinder {
-
         private AvrcpControllerService mService;
 
         private AvrcpControllerService getService() {
@@ -460,14 +201,14 @@
                 return null;
             }
 
-            if (mService != null && mService.isAvailable()) {
+            if (mService != null) {
                 return mService;
             }
             return null;
         }
 
-        BluetoothAvrcpControllerBinder(AvrcpControllerService svc) {
-            mService = svc;
+        AvrcpControllerServiceBinder(AvrcpControllerService service) {
+            mService = service;
         }
 
         @Override
@@ -499,153 +240,102 @@
             if (service == null) {
                 return BluetoothProfile.STATE_DISCONNECTED;
             }
-
-            if (device == null) {
-                throw new IllegalStateException("Device cannot be null!");
-            }
-
             return service.getConnectionState(device);
         }
 
         @Override
         public void sendGroupNavigationCmd(BluetoothDevice device, int keyCode, int keyState) {
-            Log.v(TAG, "Binder Call: sendGroupNavigationCmd");
-            AvrcpControllerService service = getService();
-            if (service == null) {
-                return;
-            }
+            Log.w(TAG, "sendGroupNavigationCmd not implemented");
+            return;
+        }
 
-            if (device == null) {
-                throw new IllegalStateException("Device cannot be null!");
-            }
-
-            service.sendGroupNavigationCmd(device, keyCode, keyState);
+        @Override
+        public boolean setPlayerApplicationSetting(BluetoothAvrcpPlayerSettings settings) {
+            Log.w(TAG, "setPlayerApplicationSetting not implemented");
+            return false;
         }
 
         @Override
         public BluetoothAvrcpPlayerSettings getPlayerSettings(BluetoothDevice device) {
-            Log.v(TAG, "Binder Call: getPlayerApplicationSetting ");
-            AvrcpControllerService service = getService();
-            if (service == null) {
-                return null;
-            }
-
-            if (device == null) {
-                throw new IllegalStateException("Device cannot be null!");
-            }
-
-            return service.getPlayerSettings(device);
-        }
-
-        @Override
-        public boolean setPlayerApplicationSetting(BluetoothAvrcpPlayerSettings plAppSetting) {
-            Log.v(TAG, "Binder Call: setPlayerApplicationSetting ");
-            AvrcpControllerService service = getService();
-            if (service == null) {
-                return false;
-            }
-            return service.setPlayerApplicationSetting(plAppSetting);
+            Log.w(TAG, "getPlayerSettings not implemented");
+            return null;
         }
     }
 
+
+    /* JNI API*/
     // Called by JNI when a passthrough key was received.
     private void handlePassthroughRsp(int id, int keyState, byte[] address) {
-        Log.d(TAG,
-                "passthrough response received as: key: " + id + " state: " + keyState + "address:"
-                        + address);
+        if (DBG) {
+            Log.d(TAG, "passthrough response received as: key: " + id
+                    + " state: " + keyState + "address:" + address);
+        }
     }
 
     private void handleGroupNavigationRsp(int id, int keyState) {
-        Log.d(TAG, "group navigation response received as: key: " + id + " state: " + keyState);
+        if (DBG) {
+            Log.d(TAG, "group navigation response received as: key: " + id + " state: "
+                    + keyState);
+        }
     }
 
     // Called by JNI when a device has connected or disconnected.
-    private synchronized void onConnectionStateChanged(boolean rcConnected, boolean brConnected,
-            byte[] address) {
+    private synchronized void onConnectionStateChanged(boolean remoteControlConnected,
+            boolean browsingConnected, byte[] address) {
         BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
-        Log.d(TAG, "onConnectionStateChanged " + rcConnected + " " + brConnected + device
-                + " conn device " + mConnectedDevice);
+        if (DBG) {
+            Log.d(TAG, "onConnectionStateChanged " + remoteControlConnected + " "
+                    + browsingConnected + device);
+        }
         if (device == null) {
             Log.e(TAG, "onConnectionStateChanged Device is null");
             return;
         }
 
-        // Adjust the AVRCP connection state.
-        int oldState = (device.equals(mConnectedDevice) ? BluetoothProfile.STATE_CONNECTED
-                : BluetoothProfile.STATE_DISCONNECTED);
-        int newState = (rcConnected ? BluetoothProfile.STATE_CONNECTED
-                : BluetoothProfile.STATE_DISCONNECTED);
-
-        if (rcConnected && oldState == BluetoothProfile.STATE_DISCONNECTED) {
-            /* AVRCPControllerService supports single connection */
-            if (mConnectedDevice != null) {
-                Log.d(TAG, "A Connection already exists, returning");
-                return;
-            }
-            mConnectedDevice = device;
-            Message msg = mAvrcpCtSm.obtainMessage(
-                    AvrcpControllerStateMachine.MESSAGE_PROCESS_CONNECTION_CHANGE, newState,
-                    oldState, device);
-            mAvrcpCtSm.sendMessage(msg);
-        } else if (!rcConnected && oldState == BluetoothProfile.STATE_CONNECTED) {
-            mConnectedDevice = null;
-            Message msg = mAvrcpCtSm.obtainMessage(
-                    AvrcpControllerStateMachine.MESSAGE_PROCESS_CONNECTION_CHANGE, newState,
-                    oldState, device);
-            mAvrcpCtSm.sendMessage(msg);
-        }
-
-        // Adjust the browse connection state. If RC is connected we should have already sent the
-        // connection status out.
-        if (rcConnected && brConnected) {
-            mBrowseConnected = true;
-            Message msg = mAvrcpCtSm.obtainMessage(
-                    AvrcpControllerStateMachine.MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE);
-            msg.arg1 = 1;
-            msg.obj = device;
-            mAvrcpCtSm.sendMessage(msg);
+        StackEvent event =
+                StackEvent.connectionStateChanged(remoteControlConnected, browsingConnected);
+        AvrcpControllerStateMachine stateMachine = getOrCreateStateMachine(device);
+        if (remoteControlConnected || browsingConnected) {
+            stateMachine.connect(event);
+        } else {
+            stateMachine.disconnect();
         }
     }
 
     // Called by JNI to notify Avrcp of features supported by the Remote device.
     private void getRcFeatures(byte[] address, int features) {
-        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
-        Message msg =
-                mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_RC_FEATURES,
-                        features, 0, device);
-        mAvrcpCtSm.sendMessage(msg);
+        /* Do Nothing. */
     }
 
     // Called by JNI
     private void setPlayerAppSettingRsp(byte[] address, byte accepted) {
-              /* Do Nothing. */
+        /* Do Nothing. */
     }
 
     // Called by JNI when remote wants to receive absolute volume notifications.
     private synchronized void handleRegisterNotificationAbsVol(byte[] address, byte label) {
-        Log.d(TAG, "handleRegisterNotificationAbsVol ");
-        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
-        if (device != null && !device.equals(mConnectedDevice)) {
-            Log.e(TAG, "handleRegisterNotificationAbsVol device not found " + address);
-            return;
+        if (DBG) {
+            Log.d(TAG, "handleRegisterNotificationAbsVol");
         }
-        Message msg = mAvrcpCtSm.obtainMessage(
-                AvrcpControllerStateMachine.MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION,
-                (int) label, 0);
-        mAvrcpCtSm.sendMessage(msg);
+        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+        AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+        if (stateMachine != null) {
+            stateMachine.sendMessage(
+                    AvrcpControllerStateMachine.MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION);
+        }
     }
 
     // Called by JNI when remote wants to set absolute volume.
     private synchronized void handleSetAbsVolume(byte[] address, byte absVol, byte label) {
-        Log.d(TAG, "handleSetAbsVolume ");
-        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
-        if (device != null && !device.equals(mConnectedDevice)) {
-            Log.e(TAG, "handleSetAbsVolume device not found " + address);
-            return;
+        if (DBG) {
+            Log.d(TAG, "handleSetAbsVolume ");
         }
-        Message msg = mAvrcpCtSm.obtainMessage(
-                AvrcpControllerStateMachine.MESSAGE_PROCESS_SET_ABS_VOL_CMD, absVol, label);
-        mAvrcpCtSm.sendMessage(msg);
+        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+        AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+        if (stateMachine != null) {
+            stateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_SET_ABS_VOL_CMD,
+                    absVol);
+        }
     }
 
     // Called by JNI when a track changes and local AvrcpController is registered for updates.
@@ -654,24 +344,13 @@
         if (DBG) {
             Log.d(TAG, "onTrackChanged");
         }
-        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
-        if (device != null && !device.equals(mConnectedDevice)) {
-            Log.e(TAG, "onTrackChanged device not found " + address);
-            return;
-        }
 
-        List<Integer> attrList = new ArrayList<>();
-        for (int attr : attributes) {
-            attrList.add(attr);
+        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+        AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+        if (stateMachine != null) {
+            stateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_TRACK_CHANGED,
+                    TrackInfo.getMetadata(attributes, attribVals));
         }
-        List<String> attrValList = Arrays.asList(attribVals);
-        TrackInfo trackInfo = new TrackInfo(attrList, attrValList);
-        if (VDBG) {
-            Log.d(TAG, "onTrackChanged " + trackInfo);
-        }
-        Message msg = mAvrcpCtSm.obtainMessage(
-                AvrcpControllerStateMachine.MESSAGE_PROCESS_TRACK_CHANGED, trackInfo);
-        mAvrcpCtSm.sendMessage(msg);
     }
 
     // Called by JNI periodically based upon timer to update play position
@@ -681,14 +360,12 @@
             Log.d(TAG, "onPlayPositionChanged pos " + currSongPosition);
         }
         BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
-        if (device != null && !device.equals(mConnectedDevice)) {
-            Log.e(TAG, "onPlayPositionChanged not found device not found " + address);
-            return;
+        AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+        if (stateMachine != null) {
+            stateMachine.sendMessage(
+                    AvrcpControllerStateMachine.MESSAGE_PROCESS_PLAY_POS_CHANGED,
+                    songLen, currSongPosition);
         }
-        Message msg = mAvrcpCtSm.obtainMessage(
-                AvrcpControllerStateMachine.MESSAGE_PROCESS_PLAY_POS_CHANGED,
-                songLen, currSongPosition);
-        mAvrcpCtSm.sendMessage(msg);
     }
 
     // Called by JNI on changes of play status
@@ -696,11 +373,6 @@
         if (DBG) {
             Log.d(TAG, "onPlayStatusChanged " + playStatus);
         }
-        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
-        if (device != null && !device.equals(mConnectedDevice)) {
-            Log.e(TAG, "onPlayStatusChanged not found device not found " + address);
-            return;
-        }
         int playbackState = PlaybackState.STATE_NONE;
         switch (playStatus) {
             case JNI_PLAY_STATUS_STOPPED:
@@ -721,9 +393,12 @@
             default:
                 playbackState = PlaybackState.STATE_NONE;
         }
-        Message msg = mAvrcpCtSm.obtainMessage(
-                AvrcpControllerStateMachine.MESSAGE_PROCESS_PLAY_STATUS_CHANGED, playbackState);
-        mAvrcpCtSm.sendMessage(msg);
+        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+        AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+        if (stateMachine != null) {
+            stateMachine.sendMessage(
+                    AvrcpControllerStateMachine.MESSAGE_PROCESS_PLAY_STATUS_CHANGED, playbackState);
+        }
     }
 
     // Called by JNI to report remote Player's capabilities
@@ -733,13 +408,13 @@
             Log.d(TAG, "handlePlayerAppSetting rspLen = " + rspLen);
         }
         BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
-        if (device != null && !device.equals(mConnectedDevice)) {
-            Log.e(TAG, "handlePlayerAppSetting not found device not found " + address);
-            return;
+        AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+        if (stateMachine != null) {
+            PlayerApplicationSettings supportedSettings =
+                    PlayerApplicationSettings.makeSupportedSettings(playerAttribRsp);
         }
-        PlayerApplicationSettings supportedSettings =
-                PlayerApplicationSettings.makeSupportedSettings(playerAttribRsp);
         /* Do nothing */
+
     }
 
     private synchronized void onPlayerAppSettingChanged(byte[] address, byte[] playerAttribRsp,
@@ -748,50 +423,48 @@
             Log.d(TAG, "onPlayerAppSettingChanged ");
         }
         BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
-        if (device != null && !device.equals(mConnectedDevice)) {
-            Log.e(TAG, "onPlayerAppSettingChanged not found device not found " + address);
-            return;
+        AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+        if (stateMachine != null) {
+
+            PlayerApplicationSettings desiredSettings =
+                    PlayerApplicationSettings.makeSettings(playerAttribRsp);
         }
-        PlayerApplicationSettings desiredSettings =
-                PlayerApplicationSettings.makeSettings(playerAttribRsp);
         /* Do nothing */
     }
 
     // Browsing related JNI callbacks.
-    void handleGetFolderItemsRsp(int status, MediaItem[] items) {
+    void handleGetFolderItemsRsp(byte[] address, int status, MediaItem[] items) {
         if (DBG) {
             Log.d(TAG, "handleGetFolderItemsRsp called with status " + status + " items "
                     + items.length + " items.");
         }
-
-        if (status == JNI_AVRC_INV_RANGE) {
-            Log.w(TAG, "Sending out of range message.");
-            // Send a special message since this could be used by state machine
-            // to take as a signal that fetch is finished.
-            Message msg = mAvrcpCtSm.obtainMessage(
-                    AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE);
-            mAvrcpCtSm.sendMessage(msg);
-            return;
-        }
-
         for (MediaItem item : items) {
             if (VDBG) {
-                Log.d(TAG, "media item: " + item + " uid: " + item.getDescription().getMediaId());
+                Log.d(TAG, "media item: " + item + " uid: "
+                        + item.getDescription().getMediaId());
             }
         }
         ArrayList<MediaItem> itemsList = new ArrayList<>();
         for (MediaItem item : items) {
             itemsList.add(item);
         }
-        Message msg = mAvrcpCtSm.obtainMessage(
-                AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS, itemsList);
-        mAvrcpCtSm.sendMessage(msg);
+
+        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+
+        AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+        if (stateMachine != null) {
+
+            stateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS,
+                    itemsList);
+        }
     }
 
-    void handleGetPlayerItemsRsp(AvrcpPlayer[] items) {
+
+    void handleGetPlayerItemsRsp(byte[] address, AvrcpPlayer[] items) {
         if (DBG) {
             Log.d(TAG, "handleGetFolderItemsRsp called with " + items.length + " items.");
         }
+
         for (AvrcpPlayer item : items) {
             if (VDBG) {
                 Log.d(TAG, "bt player item: " + item);
@@ -802,28 +475,31 @@
             itemsList.add(p);
         }
 
-        Message msg = mAvrcpCtSm.obtainMessage(
-                AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_PLAYER_ITEMS, itemsList);
-        mAvrcpCtSm.sendMessage(msg);
+        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+        AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+        if (stateMachine != null) {
+            stateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_PLAYER_ITEMS,
+                    itemsList);
+        }
     }
 
     // JNI Helper functions to convert native objects to java.
-    MediaItem createFromNativeMediaItem(byte[] uid, int type, String name, int[] attrIds,
+    MediaItem createFromNativeMediaItem(long uid, int type, String name, int[] attrIds,
             String[] attrVals) {
         if (VDBG) {
-            Log.d(TAG, "createFromNativeMediaItem uid: " + uid + " type " + type + " name " + name
-                    + " attrids " + attrIds + " attrVals " + attrVals);
+            Log.d(TAG, "createFromNativeMediaItem uid: " + uid + " type " + type + " name "
+                    + name + " attrids " + attrIds + " attrVals " + attrVals);
         }
         MediaDescription.Builder mdb = new MediaDescription.Builder();
 
         Bundle mdExtra = new Bundle();
-        mdExtra.putString(MEDIA_ITEM_UID_KEY, byteUIDToHexString(uid));
+        mdExtra.putLong(MEDIA_ITEM_UID_KEY, uid);
         mdb.setExtras(mdExtra);
 
+
         // Generate a random UUID. We do this since database unaware TGs can send multiple
         // items with same MEDIA_ITEM_UID_KEY.
         mdb.setMediaId(UUID.randomUUID().toString());
-
         // Concise readable name.
         mdb.setTitle(name);
 
@@ -833,23 +509,20 @@
         return new MediaItem(mdb.build(), MediaItem.FLAG_PLAYABLE);
     }
 
-    MediaItem createFromNativeFolderItem(byte[] uid, int type, String name, int playable) {
+    MediaItem createFromNativeFolderItem(long uid, int type, String name, int playable) {
         if (VDBG) {
-            Log.d(TAG, "createFromNativeFolderItem uid: " + uid + " type " + type + " name " + name
-                    + " playable " + playable);
+            Log.d(TAG, "createFromNativeFolderItem uid: " + uid + " type " + type + " name "
+                    + name + " playable " + playable);
         }
         MediaDescription.Builder mdb = new MediaDescription.Builder();
 
-        // Covert the byte to a hex string. The coversion can be done back here to a
-        // byte array when needed.
         Bundle mdExtra = new Bundle();
-        mdExtra.putString(MEDIA_ITEM_UID_KEY, byteUIDToHexString(uid));
+        mdExtra.putLong(MEDIA_ITEM_UID_KEY, uid);
         mdb.setExtras(mdExtra);
 
         // Generate a random UUID. We do this since database unaware TGs can send multiple
         // items with same MEDIA_ITEM_UID_KEY.
         mdb.setMediaId(UUID.randomUUID().toString());
-
         // Concise readable name.
         mdb.setTitle(name);
 
@@ -860,90 +533,172 @@
             int playStatus, int playerType) {
         if (VDBG) {
             Log.d(TAG,
-                    "createFromNativePlayerItem name: " + name + " transportFlags " + transportFlags
-                            + " play status " + playStatus + " player type " + playerType);
+                    "createFromNativePlayerItem name: " + name + " transportFlags "
+                            + transportFlags + " play status " + playStatus + " player type "
+                            + playerType);
         }
         AvrcpPlayer player = new AvrcpPlayer(id, name, transportFlags, playStatus, playerType);
         return player;
     }
 
-    private void handleChangeFolderRsp(int count) {
+    private void handleChangeFolderRsp(byte[] address, int count) {
         if (DBG) {
             Log.d(TAG, "handleChangeFolderRsp count: " + count);
         }
-        Message msg =
-                mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_FOLDER_PATH,
-                        count);
-        mAvrcpCtSm.sendMessage(msg);
+        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+        AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+        if (stateMachine != null) {
+            stateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_FOLDER_PATH,
+                    count);
+        }
     }
 
-    private void handleSetBrowsedPlayerRsp(int items, int depth) {
+    private void handleSetBrowsedPlayerRsp(byte[] address, int items, int depth) {
         if (DBG) {
             Log.d(TAG, "handleSetBrowsedPlayerRsp depth: " + depth);
         }
-        Message msg = mAvrcpCtSm.obtainMessage(
-                AvrcpControllerStateMachine.MESSAGE_PROCESS_SET_BROWSED_PLAYER, items, depth);
-        mAvrcpCtSm.sendMessage(msg);
+        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+
+        AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+        if (stateMachine != null) {
+            stateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_SET_BROWSED_PLAYER,
+                    items, depth);
+        }
     }
 
-    private void handleSetAddressedPlayerRsp(int status) {
+    private void handleSetAddressedPlayerRsp(byte[] address, int status) {
         if (DBG) {
             Log.d(TAG, "handleSetAddressedPlayerRsp status: " + status);
         }
-        Message msg = mAvrcpCtSm.obtainMessage(
-                AvrcpControllerStateMachine.MESSAGE_PROCESS_SET_ADDRESSED_PLAYER);
-        mAvrcpCtSm.sendMessage(msg);
+        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+
+        AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+        if (stateMachine != null) {
+            stateMachine.sendMessage(
+                    AvrcpControllerStateMachine.MESSAGE_PROCESS_SET_ADDRESSED_PLAYER);
+        }
     }
 
-    private void handleAddressedPlayerChanged(int id) {
+    private void handleAddressedPlayerChanged(byte[] address, int id) {
         if (DBG) {
             Log.d(TAG, "handleAddressedPlayerChanged id: " + id);
         }
-        Message msg = mAvrcpCtSm.obtainMessage(
-                AvrcpControllerStateMachine.MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED, id);
-        mAvrcpCtSm.sendMessage(msg);
+        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+
+        AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+        if (stateMachine != null) {
+            stateMachine.sendMessage(
+                    AvrcpControllerStateMachine.MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED, id);
+        }
     }
 
-    private void handleNowPlayingContentChanged() {
+    private void handleNowPlayingContentChanged(byte[] address) {
         if (DBG) {
             Log.d(TAG, "handleNowPlayingContentChanged");
         }
-        mAvrcpCtSm.sendMessage(
-                AvrcpControllerStateMachine.MESSAGE_PROCESS_NOW_PLAYING_CONTENTS_CHANGED);
+        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+
+        AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+        if (stateMachine != null) {
+            stateMachine.nowPlayingContentChanged();
+        }
+    }
+
+    /* Generic Profile Code */
+
+    /**
+     * Disconnect the given Bluetooth device.
+     *
+     * @return true if disconnect is successful, false otherwise.
+     */
+    public synchronized boolean disconnect(BluetoothDevice device) {
+        if (DBG) {
+            StringBuilder sb = new StringBuilder();
+            dump(sb);
+            Log.d(TAG, "MAP disconnect device: " + device
+                    + ", InstanceMap start state: " + sb.toString());
+        }
+        AvrcpControllerStateMachine stateMachine = mDeviceStateMap.get(device);
+        // a map state machine instance doesn't exist. maybe it is already gone?
+        if (stateMachine == null) {
+            return false;
+        }
+        int connectionState = stateMachine.getState();
+        if (connectionState != BluetoothProfile.STATE_CONNECTED
+                && connectionState != BluetoothProfile.STATE_CONNECTING) {
+            return false;
+        }
+        stateMachine.disconnect();
+        if (DBG) {
+            StringBuilder sb = new StringBuilder();
+            dump(sb);
+            Log.d(TAG, "MAP disconnect device: " + device
+                    + ", InstanceMap start state: " + sb.toString());
+        }
+        return true;
+    }
+
+    void removeStateMachine(AvrcpControllerStateMachine stateMachine) {
+        mDeviceStateMap.remove(stateMachine.getDevice());
+    }
+
+    public List<BluetoothDevice> getConnectedDevices() {
+        return getDevicesMatchingConnectionStates(new int[]{BluetoothAdapter.STATE_CONNECTED});
+    }
+
+    protected AvrcpControllerStateMachine getStateMachine(BluetoothDevice device) {
+        return mDeviceStateMap.get(device);
+    }
+
+    protected AvrcpControllerStateMachine getOrCreateStateMachine(BluetoothDevice device) {
+        AvrcpControllerStateMachine stateMachine = mDeviceStateMap.get(device);
+        if (stateMachine == null) {
+            stateMachine = newStateMachine(device);
+            mDeviceStateMap.put(device, stateMachine);
+            stateMachine.start();
+        }
+        return stateMachine;
+    }
+
+    List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+        if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states));
+        List<BluetoothDevice> deviceList = new ArrayList<>();
+        Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices();
+        int connectionState;
+        for (BluetoothDevice device : bondedDevices) {
+            connectionState = getConnectionState(device);
+            if (DBG) Log.d(TAG, "Device: " + device + "State: " + connectionState);
+            for (int i = 0; i < states.length; i++) {
+                if (connectionState == states[i]) {
+                    deviceList.add(device);
+                }
+            }
+        }
+        if (DBG) Log.d(TAG, deviceList.toString());
+        Log.d(TAG, "GetDevicesDone");
+        return deviceList;
+    }
+
+    synchronized int getConnectionState(BluetoothDevice device) {
+        AvrcpControllerStateMachine stateMachine = mDeviceStateMap.get(device);
+        return (stateMachine == null) ? BluetoothProfile.STATE_DISCONNECTED
+                : stateMachine.getState();
     }
 
     @Override
     public void dump(StringBuilder sb) {
         super.dump(sb);
-        mAvrcpCtSm.dump(sb);
+        ProfileService.println(sb, "Devices Tracked = " + mDeviceStateMap.size());
+
+        for (AvrcpControllerStateMachine stateMachine : mDeviceStateMap.values()) {
+            ProfileService.println(sb,
+                    "==== StateMachine for " + stateMachine.getDevice() + " ====");
+            stateMachine.dump(sb);
+        }
+        sb.append("\n  sBrowseTree: " + sBrowseTree.toString());
     }
 
-    public static String byteUIDToHexString(byte[] uid) {
-        StringBuilder sb = new StringBuilder();
-        for (byte b : uid) {
-            sb.append(String.format("%02X", b));
-        }
-        return sb.toString();
-    }
-
-    public static byte[] hexStringToByteUID(String uidStr) {
-        if (uidStr == null) {
-            Log.e(TAG, "Null hex string.");
-            return EMPTY_UID;
-        } else if (uidStr.length() % 2 == 1) {
-            // Odd length strings should not be possible.
-            Log.e(TAG, "Odd length hex string " + uidStr);
-            return EMPTY_UID;
-        }
-        int len = uidStr.length();
-        byte[] data = new byte[len / 2];
-        for (int i = 0; i < len; i += 2) {
-            data[i / 2] = (byte) ((Character.digit(uidStr.charAt(i), 16) << 4) + Character.digit(
-                    uidStr.charAt(i + 1), 16));
-        }
-        return data;
-    }
-
+    /*JNI*/
     private static native void classInitNative();
 
     private native void initNative();
@@ -978,9 +733,9 @@
     static native void getPlayerListNative(byte[] address, int start, int end);
 
     /* API used to change the folder */
-    static native void changeFolderPathNative(byte[] address, byte direction, byte[] uid);
+    static native void changeFolderPathNative(byte[] address, byte direction, long uid);
 
-    static native void playItemNative(byte[] address, byte scope, byte[] uid, int uidCounter);
+    static native void playItemNative(byte[] address, byte scope, long uid, int uidCounter);
 
     static native void setBrowsedPlayerNative(byte[] address, int playerId);
 
diff --git a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java
index f13f0c4..13ee58d 100644
--- a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java
+++ b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java
@@ -19,19 +19,20 @@
 import android.bluetooth.BluetoothAvrcpController;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
-import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.media.AudioManager;
 import android.media.MediaMetadata;
 import android.media.browse.MediaBrowser.MediaItem;
+import android.media.session.MediaSession;
 import android.media.session.PlaybackState;
+import android.os.Bundle;
 import android.os.Message;
 import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.bluetooth.BluetoothMetricsProto;
+import com.android.bluetooth.R;
 import com.android.bluetooth.Utils;
 import com.android.bluetooth.a2dpsink.A2dpSinkService;
 import com.android.bluetooth.btservice.MetricsLogger;
@@ -41,383 +42,375 @@
 
 import java.util.ArrayList;
 import java.util.List;
-
 /**
  * Provides Bluetooth AVRCP Controller State Machine responsible for all remote control connections
  * and interactions with a remote controlable device.
  */
 class AvrcpControllerStateMachine extends StateMachine {
+    static final String TAG = "AvrcpControllerStateMachine";
+    static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
 
-    // commands from Binder service
-    static final int MESSAGE_SEND_PASS_THROUGH_CMD = 1;
-    static final int MESSAGE_SEND_GROUP_NAVIGATION_CMD = 3;
-    static final int MESSAGE_GET_FOLDER_LIST = 6;
-    static final int MESSAGE_FETCH_ATTR_AND_PLAY_ITEM = 9;
+    //0->99 Events from Outside
+    public static final int CONNECT = 1;
+    public static final int DISCONNECT = 2;
 
-    // commands from native layer
-    static final int MESSAGE_PROCESS_SET_ABS_VOL_CMD = 103;
-    static final int MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION = 104;
-    static final int MESSAGE_PROCESS_TRACK_CHANGED = 105;
-    static final int MESSAGE_PROCESS_PLAY_POS_CHANGED = 106;
-    static final int MESSAGE_PROCESS_PLAY_STATUS_CHANGED = 107;
-    static final int MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION = 108;
-    static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS = 109;
-    static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE = 110;
-    static final int MESSAGE_PROCESS_GET_PLAYER_ITEMS = 111;
-    static final int MESSAGE_PROCESS_FOLDER_PATH = 112;
-    static final int MESSAGE_PROCESS_SET_BROWSED_PLAYER = 113;
-    static final int MESSAGE_PROCESS_SET_ADDRESSED_PLAYER = 114;
-    static final int MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED = 115;
-    static final int MESSAGE_PROCESS_NOW_PLAYING_CONTENTS_CHANGED = 116;
+    //100->199 Internal Events
+    protected static final int CLEANUP = 100;
+    private static final int CONNECT_TIMEOUT = 101;
 
-    // commands for connection
-    static final int MESSAGE_PROCESS_RC_FEATURES = 301;
-    static final int MESSAGE_PROCESS_CONNECTION_CHANGE = 302;
-    static final int MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE = 303;
+    //200->299 Events from Native
+    static final int STACK_EVENT = 200;
+    static final int MESSAGE_INTERNAL_CMD_TIMEOUT = 201;
 
-    // Interal messages
-    static final int MESSAGE_INTERNAL_CMD_TIMEOUT = 403;
+    static final int MESSAGE_PROCESS_SET_ABS_VOL_CMD = 203;
+    static final int MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION = 204;
+    static final int MESSAGE_PROCESS_TRACK_CHANGED = 205;
+    static final int MESSAGE_PROCESS_PLAY_POS_CHANGED = 206;
+    static final int MESSAGE_PROCESS_PLAY_STATUS_CHANGED = 207;
+    static final int MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION = 208;
+    static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS = 209;
+    static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE = 210;
+    static final int MESSAGE_PROCESS_GET_PLAYER_ITEMS = 211;
+    static final int MESSAGE_PROCESS_FOLDER_PATH = 212;
+    static final int MESSAGE_PROCESS_SET_BROWSED_PLAYER = 213;
+    static final int MESSAGE_PROCESS_SET_ADDRESSED_PLAYER = 214;
+    static final int MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED = 215;
+    static final int MESSAGE_PROCESS_NOW_PLAYING_CONTENTS_CHANGED = 216;
+
+    //300->399 Events for Browsing
+    static final int MESSAGE_GET_FOLDER_ITEMS = 300;
+    static final int MESSAGE_PLAY_ITEM = 301;
+    static final int MSG_AVRCP_PASSTHRU = 302;
+
     static final int MESSAGE_INTERNAL_ABS_VOL_TIMEOUT = 404;
 
-    static final int ABS_VOL_TIMEOUT_MILLIS = 1000; //1s
-    static final int CMD_TIMEOUT_MILLIS = 5000; // 5s
-    // Fetch only 20 items at a time.
-    static final int GET_FOLDER_ITEMS_PAGINATION_SIZE = 20;
-    // Fetch no more than 1000 items per directory.
-    static final int MAX_FOLDER_ITEMS = 1000;
-
     /*
      * Base value for absolute volume from JNI
      */
     private static final int ABS_VOL_BASE = 127;
 
-    /*
-     * Notification types for Avrcp protocol JNI.
-     */
-    private static final byte NOTIFICATION_RSP_TYPE_INTERIM = 0x00;
-    private static final byte NOTIFICATION_RSP_TYPE_CHANGED = 0x01;
-
-
-    private static final String TAG = "AvrcpControllerSM";
-    private static final boolean DBG = true;
-    private static final boolean VDBG = false;
-
-    private final Context mContext;
     private final AudioManager mAudioManager;
 
-    private final State mDisconnected;
-    private final State mConnected;
-    private final SetAddresedPlayerAndPlayItem mSetAddrPlayer;
-    private final GetFolderList mGetFolderList;
+    protected final BluetoothDevice mDevice;
+    protected final byte[] mDeviceAddress;
+    protected final AvrcpControllerService mService;
+    protected final Disconnected mDisconnected;
+    protected final Connecting mConnecting;
+    protected final Connected mConnected;
+    protected final Disconnecting mDisconnecting;
 
-    private final Object mLock = new Object();
-    private static final MediaMetadata EMPTY_MEDIA_METADATA = new MediaMetadata.Builder().build();
+    protected int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
 
-    // APIs exist to access these so they must be thread safe
-    private Boolean mIsConnected = false;
-    private RemoteDevice mRemoteDevice;
-    private AvrcpPlayer mAddressedPlayer;
-
-    // Only accessed from State Machine processMessage
-    private int mVolumeChangedNotificationsToIgnore = 0;
-    private int mPreviousPercentageVol = -1;
-    private int mAddressedPlayerID = -1;
+    boolean mRemoteControlConnected = false;
+    boolean mBrowsingConnected = false;
+    BrowseTree mBrowseTree = null;
+    private AvrcpPlayer mAddressedPlayer = new AvrcpPlayer();
     private SparseArray<AvrcpPlayer> mAvailablePlayerList = new SparseArray<AvrcpPlayer>();
+    private int mVolumeChangedNotificationsToIgnore = 0;
 
-    // Browse tree.
-    private BrowseTree mBrowseTree = new BrowseTree();
+    GetFolderList mGetFolderList = null;
 
-    AvrcpControllerStateMachine(Context context) {
+    //Number of items to get in a single fetch
+    static final int ITEM_PAGE_SIZE = 20;
+    static final int CMD_TIMEOUT_MILLIS = 10000;
+    static final int ABS_VOL_TIMEOUT_MILLIS = 1000; //1s
+
+    AvrcpControllerStateMachine(BluetoothDevice device, AvrcpControllerService service) {
         super(TAG);
-        mContext = context;
-
-        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
-        IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
-        mContext.registerReceiver(mBroadcastReceiver, filter);
+        mDevice = device;
+        mDeviceAddress = Utils.getByteAddress(mDevice);
+        mService = service;
+        logD(device.toString());
 
         mDisconnected = new Disconnected();
+        mConnecting = new Connecting();
         mConnected = new Connected();
-
-        // Used to change folder path and fetch the new folder listing.
-        mSetAddrPlayer = new SetAddresedPlayerAndPlayItem();
-        mGetFolderList = new GetFolderList();
+        mDisconnecting = new Disconnecting();
 
         addState(mDisconnected);
+        addState(mConnecting);
         addState(mConnected);
+        addState(mDisconnecting);
 
-        // Any action that needs blocking other requests to the state machine will be implemented as
-        // a separate substate of the mConnected state. Once transtition to the sub-state we should
-        // only handle the messages that are relevant to the sub-action. Everything else should be
-        // deferred so that once we transition to the mConnected we can process them hence.
-        addState(mSetAddrPlayer, mConnected);
+        mGetFolderList = new GetFolderList();
         addState(mGetFolderList, mConnected);
+        mAudioManager = (AudioManager) service.getSystemService(Context.AUDIO_SERVICE);
 
         setInitialState(mDisconnected);
     }
 
-    class Disconnected extends State {
+    BrowseTree.BrowseNode findNode(String parentMediaId) {
+        logD("FindNode");
+        return mBrowseTree.findBrowseNodeByID(parentMediaId);
+    }
+
+    /**
+     * Get the current connection state
+     *
+     * @return current State
+     */
+    public int getState() {
+        return mMostRecentState;
+    }
+
+    /**
+     * Get the underlying device tracked by this state machine
+     *
+     * @return device in focus
+     */
+    public synchronized BluetoothDevice getDevice() {
+        return mDevice;
+    }
+
+    /**
+     * send the connection event asynchronously
+     */
+    public boolean connect(StackEvent event) {
+        sendMessage(CONNECT);
+        if (!mBrowsingConnected && event.mBrowsingConnected) {
+            onBrowsingConnected();
+        }
+        mRemoteControlConnected = event.mRemoteControlConnected;
+        mBrowsingConnected = event.mBrowsingConnected;
+        return true;
+    }
+
+    /**
+     * send the Disconnect command asynchronously
+     */
+    public void disconnect() {
+        sendMessage(DISCONNECT);
+    }
+
+    /**
+     * Dump the current State Machine to the string builder.
+     *
+     * @param sb output string
+     */
+    public void dump(StringBuilder sb) {
+        ProfileService.println(sb, "mDevice: " + mDevice.getAddress() + "("
+                + mDevice.getName() + ") " + this.toString());
+    }
+
+    @Override
+    protected void unhandledMessage(Message msg) {
+        Log.w(TAG, "Unhandled message in state " + getCurrentState() + "msg.what=" + msg.what);
+    }
+
+    private static void logD(String message) {
+        if (DBG) {
+            Log.d(TAG, message);
+        }
+    }
+
+    void onBrowsingConnected() {
+        mBrowseTree = new BrowseTree(mDevice);
+        mService.sBrowseTree.mRootNode.addChild(mBrowseTree.mRootNode);
+        BluetoothMediaBrowserService.notifyChanged(mService
+                .sBrowseTree.mRootNode);
+        BluetoothMediaBrowserService.notifyChanged(mAddressedPlayer.getPlaybackState());
+    }
+
+    void onBrowsingDisconnected() {
+        mAddressedPlayer.setPlayStatus(PlaybackState.STATE_ERROR);
+        mAddressedPlayer.updateCurrentTrack(null);
+        mBrowseTree.mNowPlayingNode.setCached(false);
+        BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mNowPlayingNode);
+        PlaybackState.Builder pbb = new PlaybackState.Builder();
+        pbb.setState(PlaybackState.STATE_ERROR, PlaybackState.PLAYBACK_POSITION_UNKNOWN,
+                1.0f).setActions(0);
+        BluetoothMediaBrowserService.notifyChanged(pbb.build());
+        mService.sBrowseTree.mRootNode.removeChild(
+                mBrowseTree.mRootNode);
+        BluetoothMediaBrowserService.notifyChanged(mService
+                .sBrowseTree.mRootNode);
+        BluetoothMediaBrowserService.trackChanged(null);
+
+    }
+
+    private void notifyChanged(BrowseTree.BrowseNode node) {
+        BluetoothMediaBrowserService.notifyChanged(node);
+    }
+
+    void requestContents(BrowseTree.BrowseNode node) {
+        sendMessage(MESSAGE_GET_FOLDER_ITEMS, node);
+
+        logD("Fetching " + node);
+    }
+
+    void nowPlayingContentChanged() {
+        mBrowseTree.mNowPlayingNode.setCached(false);
+        sendMessage(MESSAGE_GET_FOLDER_ITEMS, mBrowseTree.mNowPlayingNode);
+    }
+
+    protected class Disconnected extends State {
+        @Override
+        public void enter() {
+            logD("Enter Disconnected");
+            if (mMostRecentState != BluetoothProfile.STATE_DISCONNECTED) {
+                sendMessage(CLEANUP);
+            }
+            broadcastConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTED);
+        }
 
         @Override
-        public boolean processMessage(Message msg) {
-            if (DBG) Log.d(TAG, " HandleMessage: " + dumpMessageString(msg.what));
-            switch (msg.what) {
-                case MESSAGE_PROCESS_CONNECTION_CHANGE:
-                    if (msg.arg1 == BluetoothProfile.STATE_CONNECTED) {
-                        mBrowseTree = new BrowseTree();
-                        transitionTo(mConnected);
-                        BluetoothDevice rtDevice = (BluetoothDevice) msg.obj;
-                        synchronized (mLock) {
-                            mRemoteDevice = new RemoteDevice(rtDevice);
-                            mAddressedPlayer = new AvrcpPlayer();
-                            mIsConnected = true;
-                        }
-                        MetricsLogger.logProfileConnectionEvent(
-                                BluetoothMetricsProto.ProfileId.AVRCP_CONTROLLER);
-                        Intent intent = new Intent(
-                                BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
-                        intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
-                                BluetoothProfile.STATE_DISCONNECTED);
-                        intent.putExtra(BluetoothProfile.EXTRA_STATE,
-                                BluetoothProfile.STATE_CONNECTED);
-                        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, rtDevice);
-                        mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
-                    }
+        public boolean processMessage(Message message) {
+            switch (message.what) {
+                case CONNECT:
+                    logD("Connect");
+                    transitionTo(mConnecting);
                     break;
-
-                default:
-                    Log.w(TAG,
-                            "Currently Disconnected not handling " + dumpMessageString(msg.what));
-                    return false;
+                case CLEANUP:
+                    mService.removeStateMachine(AvrcpControllerStateMachine.this);
+                    break;
             }
             return true;
         }
     }
 
+    protected class Connecting extends State {
+        @Override
+        public void enter() {
+            logD("Enter Connecting");
+            broadcastConnectionStateChanged(BluetoothProfile.STATE_CONNECTING);
+            transitionTo(mConnected);
+        }
+    }
+
+
     class Connected extends State {
+        private static final String STATE_TAG = "Avrcp.ConnectedAvrcpController";
+        private int mCurrentlyHeldKey = 0;
+
+        @Override
+        public void enter() {
+            if (mMostRecentState == BluetoothProfile.STATE_CONNECTING) {
+                broadcastConnectionStateChanged(BluetoothProfile.STATE_CONNECTED);
+                BluetoothMediaBrowserService.addressedPlayerChanged(mSessionCallbacks);
+            } else {
+                logD("ReEnteringConnected");
+            }
+            super.enter();
+        }
+
         @Override
         public boolean processMessage(Message msg) {
-            if (DBG) Log.d(TAG, " HandleMessage: " + dumpMessageString(msg.what));
-            A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();
-            synchronized (mLock) {
-                switch (msg.what) {
-                    case MESSAGE_SEND_PASS_THROUGH_CMD:
-                        BluetoothDevice device = (BluetoothDevice) msg.obj;
-                        AvrcpControllerService.sendPassThroughCommandNative(
-                                Utils.getByteAddress(device), msg.arg1, msg.arg2);
-                        if (a2dpSinkService != null) {
-                            if (DBG) Log.d(TAG, " inform AVRCP Commands to A2DP Sink ");
-                            a2dpSinkService.informAvrcpPassThroughCmd(device, msg.arg1, msg.arg2);
-                        }
-                        break;
+            logD(STATE_TAG + " processMessage " + msg.what);
+            switch (msg.what) {
+                case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
+                    mVolumeChangedNotificationsToIgnore++;
+                    removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT);
+                    sendMessageDelayed(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT,
+                            ABS_VOL_TIMEOUT_MILLIS);
+                    setAbsVolume(msg.arg1, msg.arg2);
+                    return true;
 
-                    case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
-                        AvrcpControllerService.sendGroupNavigationCommandNative(
-                                mRemoteDevice.getBluetoothAddress(), msg.arg1, msg.arg2);
-                        break;
+                case MESSAGE_GET_FOLDER_ITEMS:
+                    transitionTo(mGetFolderList);
+                    return true;
 
-                    case MESSAGE_GET_FOLDER_LIST:
-                        // Whenever we transition we set the information for folder we need to
-                        // return result.
-                        if (DBG) Log.d(TAG, "Message_GET_FOLDER_LIST" + (String) msg.obj);
-                        mGetFolderList.setFolder((String) msg.obj);
-                        transitionTo(mGetFolderList);
-                        break;
+                case MESSAGE_PLAY_ITEM:
+                    //Set Addressed Player
+                    playItem((BrowseTree.BrowseNode) msg.obj);
+                    return true;
 
-                    case MESSAGE_FETCH_ATTR_AND_PLAY_ITEM: {
-                        int scope = msg.arg1;
-                        String playItemUid = (String) msg.obj;
-                        BrowseTree.BrowseNode currBrPlayer = mBrowseTree.getCurrentBrowsedPlayer();
-                        BrowseTree.BrowseNode currAddrPlayer =
-                                mBrowseTree.getCurrentAddressedPlayer();
-                        BrowseTree.BrowseNode itemToPlay =
-                                mBrowseTree.findBrowseNodeByID(playItemUid);
-                        if (DBG) {
-                            Log.d(TAG, "currBrPlayer " + currBrPlayer + " currAddrPlayer "
-                                    + currAddrPlayer);
-                        }
-                        if (currBrPlayer == null
-                                || currBrPlayer.equals(currAddrPlayer)
-                                || scope == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
-                            // String is encoded as a Hex String (mostly for display purposes)
-                            // hence convert this back to real byte string.
-                            // NOTE: It may be possible that sending play while the same item is
-                            // playing leads to reset of track.
-                            AvrcpControllerService.playItemNative(
-                                    mRemoteDevice.getBluetoothAddress(), (byte) scope,
-                                    AvrcpControllerService.hexStringToByteUID(playItemUid), 0);
-                        } else {
-                            // Send out the request for setting addressed player.
-                            AvrcpControllerService.setAddressedPlayerNative(
-                                    mRemoteDevice.getBluetoothAddress(),
-                                    currBrPlayer.getPlayerID());
-                            mSetAddrPlayer.setItemAndScope(currBrPlayer.getID(), playItemUid,
-                                    scope);
-                            transitionTo(mSetAddrPlayer);
-                        }
-                        break;
+                case MSG_AVRCP_PASSTHRU:
+                    passThru(msg.arg1);
+                    return true;
+
+                case MESSAGE_PROCESS_TRACK_CHANGED:
+                    mAddressedPlayer.updateCurrentTrack((MediaMetadata) msg.obj);
+                    BluetoothMediaBrowserService.trackChanged((MediaMetadata) msg.obj);
+                    return true;
+
+                case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
+                    mAddressedPlayer.setPlayStatus(msg.arg1);
+                    BluetoothMediaBrowserService.notifyChanged(mAddressedPlayer.getPlaybackState());
+                    if (mAddressedPlayer.getPlaybackState().getState()
+                            == PlaybackState.STATE_PLAYING
+                            && A2dpSinkService.getFocusState() == AudioManager.AUDIOFOCUS_NONE
+                            && !shouldRequestFocus()) {
+                        sendMessage(MSG_AVRCP_PASSTHRU,
+                                AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE);
                     }
+                    return true;
 
-                    case MESSAGE_PROCESS_CONNECTION_CHANGE:
-                        if (msg.arg1 == BluetoothProfile.STATE_DISCONNECTED) {
-                            synchronized (mLock) {
-                                mIsConnected = false;
-                                mRemoteDevice = null;
-                            }
-                            mBrowseTree.clear();
-                            transitionTo(mDisconnected);
-                            BluetoothDevice rtDevice = (BluetoothDevice) msg.obj;
-                            Intent intent = new Intent(
-                                    BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
-                            intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
-                                    BluetoothProfile.STATE_CONNECTED);
-                            intent.putExtra(BluetoothProfile.EXTRA_STATE,
-                                    BluetoothProfile.STATE_DISCONNECTED);
-                            intent.putExtra(BluetoothDevice.EXTRA_DEVICE, rtDevice);
-                            mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
-                        }
-                        break;
+                case MESSAGE_PROCESS_PLAY_POS_CHANGED:
+                    if (msg.arg2 != -1) {
+                        mAddressedPlayer.setPlayTime(msg.arg2);
 
-                    case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
-                        // Service tells us if the browse is connected or disconnected.
-                        // This is useful only for deciding whether to send browse commands rest of
-                        // the connection state handling should be done via the message
-                        // MESSAGE_PROCESS_CONNECTION_CHANGE.
-                        Intent intent = new Intent(
-                                AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED);
-                        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, (BluetoothDevice) msg.obj);
-                        if (DBG) {
-                            Log.d(TAG, "Browse connection state " + msg.arg1);
-                        }
-                        if (msg.arg1 == 1) {
-                            intent.putExtra(BluetoothProfile.EXTRA_STATE,
-                                    BluetoothProfile.STATE_CONNECTED);
-                        } else if (msg.arg1 == 0) {
-                            intent.putExtra(BluetoothProfile.EXTRA_STATE,
-                                    BluetoothProfile.STATE_DISCONNECTED);
-                        } else {
-                            Log.w(TAG, "Incorrect browse state " + msg.arg1);
-                        }
-
-                        mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
-                        break;
-
-                    case MESSAGE_PROCESS_RC_FEATURES:
-                        mRemoteDevice.setRemoteFeatures(msg.arg1);
-                        break;
-
-                    case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
-                        mVolumeChangedNotificationsToIgnore++;
-                        removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT);
-                        sendMessageDelayed(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT,
-                                ABS_VOL_TIMEOUT_MILLIS);
-                        setAbsVolume(msg.arg1, msg.arg2);
-                        break;
-
-                    case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION: {
-                        mRemoteDevice.setNotificationLabel(msg.arg1);
-                        mRemoteDevice.setAbsVolNotificationRequested(true);
-                        int percentageVol = getVolumePercentage();
-                        if (DBG) {
-                            Log.d(TAG, " Sending Interim Response = " + percentageVol + " label "
-                                    + msg.arg1);
-                        }
-                        AvrcpControllerService.sendRegisterAbsVolRspNative(
-                                mRemoteDevice.getBluetoothAddress(), NOTIFICATION_RSP_TYPE_INTERIM,
-                                percentageVol, mRemoteDevice.getNotificationLabel());
+                        BluetoothMediaBrowserService.notifyChanged(
+                                mAddressedPlayer.getPlaybackState());
                     }
-                    break;
+                    return true;
 
-                    case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION: {
-                        if (mVolumeChangedNotificationsToIgnore > 0) {
-                            mVolumeChangedNotificationsToIgnore--;
-                            if (mVolumeChangedNotificationsToIgnore == 0) {
-                                removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT);
-                            }
-                        } else {
-                            if (mRemoteDevice.getAbsVolNotificationRequested()) {
-                                int percentageVol = getVolumePercentage();
-                                if (percentageVol != mPreviousPercentageVol) {
-                                    AvrcpControllerService.sendRegisterAbsVolRspNative(
-                                            mRemoteDevice.getBluetoothAddress(),
-                                            NOTIFICATION_RSP_TYPE_CHANGED, percentageVol,
-                                            mRemoteDevice.getNotificationLabel());
-                                    mPreviousPercentageVol = percentageVol;
-                                    mRemoteDevice.setAbsVolNotificationRequested(false);
-                                }
-                            }
-                        }
-                    }
-                    break;
+                case DISCONNECT:
+                    transitionTo(mDisconnecting);
+                    return true;
 
-                    case MESSAGE_INTERNAL_ABS_VOL_TIMEOUT:
-                        // Volume changed notifications should come back promptly from the
-                        // AudioManager, if for some reason some notifications were squashed don't
-                        // prevent future notifications.
-                        if (DBG) Log.d(TAG, "Timed out on volume changed notification");
-                        mVolumeChangedNotificationsToIgnore = 0;
-                        break;
+                default:
+                    return super.processMessage(msg);
+            }
 
-                    case MESSAGE_PROCESS_TRACK_CHANGED:
-                        // Music start playing automatically and update Metadata
-                        boolean updateTrack =
-                                mAddressedPlayer.updateCurrentTrack((TrackInfo) msg.obj);
-                        if (updateTrack) {
-                            broadcastMetaDataChanged(
-                                    mAddressedPlayer.getCurrentTrack().getMediaMetaData());
-                        }
-                        break;
+        }
 
-                    case MESSAGE_PROCESS_PLAY_POS_CHANGED:
-                        if (msg.arg2 != -1) {
-                            mAddressedPlayer.setPlayTime(msg.arg2);
-                            broadcastPlayBackStateChanged(getCurrentPlayBackState());
-                        }
-                        break;
+        private void playItem(BrowseTree.BrowseNode node) {
+            if (node == null) {
+                Log.w(TAG, "Invalid item to play");
+            } else {
+                mService.playItemNative(
+                        mDeviceAddress, node.getScope(),
+                        node.getBluetoothID(), 0);
+            }
+        }
 
-                    case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
-                        int status = msg.arg1;
-                        mAddressedPlayer.setPlayStatus(status);
-                        if (status == PlaybackState.STATE_PLAYING) {
-                            a2dpSinkService.informTGStatePlaying(mRemoteDevice.mBTDevice, true);
-                        } else if (status == PlaybackState.STATE_PAUSED
-                                || status == PlaybackState.STATE_STOPPED) {
-                            a2dpSinkService.informTGStatePlaying(mRemoteDevice.mBTDevice, false);
-                        }
-                        break;
+        private synchronized void passThru(int cmd) {
+            logD("msgPassThru " + cmd);
+            // Some keys should be held until the next event.
+            if (mCurrentlyHeldKey != 0) {
+                mService.sendPassThroughCommandNative(
+                        mDeviceAddress, mCurrentlyHeldKey,
+                        AvrcpControllerService.KEY_STATE_RELEASED);
 
-                    case MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED:
-                        mAddressedPlayerID = msg.arg1;
-                        if (DBG) Log.d(TAG, "AddressedPlayer = " + mAddressedPlayerID);
-                        AvrcpPlayer updatedPlayer = mAvailablePlayerList.get(mAddressedPlayerID);
-                        if (updatedPlayer != null) {
-                            mAddressedPlayer = updatedPlayer;
-                            if (DBG) Log.d(TAG, "AddressedPlayer = " + mAddressedPlayer.getName());
-                        } else {
-                            mBrowseTree.mRootNode.setCached(false);
-                        }
-                        sendMessage(MESSAGE_PROCESS_SET_ADDRESSED_PLAYER);
-                        break;
-
-                    case MESSAGE_PROCESS_NOW_PLAYING_CONTENTS_CHANGED:
-                        mBrowseTree.mNowPlayingNode.setCached(false);
-                        mGetFolderList.setFolder(mBrowseTree.mNowPlayingNode.getID());
-                        transitionTo(mGetFolderList);
-                        break;
-
-                    default:
-                        Log.d(TAG, "Unhandled message" + msg.what);
-                        return false;
+                if (mCurrentlyHeldKey == cmd) {
+                    // Return to prevent starting FF/FR operation again
+                    mCurrentlyHeldKey = 0;
+                    return;
+                } else {
+                    // FF/FR is in progress and other operation is desired
+                    // so after stopping FF/FR, not returning so that command
+                    // can be sent for the desired operation.
+                    mCurrentlyHeldKey = 0;
                 }
             }
-            return true;
+
+            // Send the pass through.
+            mService.sendPassThroughCommandNative(mDeviceAddress, cmd,
+                    AvrcpControllerService.KEY_STATE_PRESSED);
+
+            if (isHoldableKey(cmd)) {
+                // Release cmd next time a command is sent.
+                mCurrentlyHeldKey = cmd;
+            } else {
+                mService.sendPassThroughCommandNative(mDeviceAddress,
+                        cmd, AvrcpControllerService.KEY_STATE_RELEASED);
+            }
         }
+
+        private boolean isHoldableKey(int cmd) {
+            return (cmd == AvrcpControllerService.PASS_THRU_CMD_ID_REWIND)
+                    || (cmd == AvrcpControllerService.PASS_THRU_CMD_ID_FF);
+        }
+
+
     }
 
+
     // Handle the get folder listing action
     // a) Fetch the listing of folders
     // b) Once completed return the object listing
-    class GetFolderList extends CmdState {
-        private static final String STATE_TAG = "AVRCPSM.GetFolderList";
+    class GetFolderList extends State {
+        private static final String STATE_TAG = "Avrcp.GetFolderList";
 
         boolean mAbort;
         BrowseTree.BrowseNode mBrowseNode;
@@ -425,9 +418,19 @@
 
         @Override
         public void enter() {
+            logD(STATE_TAG + " Entering GetFolderList");
             // Setup the timeouts.
+            sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
             super.enter();
             mAbort = false;
+            Message msg = getCurrentMessage();
+            if (msg.what == MESSAGE_GET_FOLDER_ITEMS) {
+                {
+                    logD(STATE_TAG + " new Get Request");
+                    mBrowseNode = (BrowseTree.BrowseNode) msg.obj;
+                }
+            }
+
             if (mBrowseNode == null) {
                 transitionTo(mConnected);
             } else {
@@ -435,28 +438,20 @@
             }
         }
 
-        public void setFolder(String id) {
-            if (DBG) Log.d(STATE_TAG, "Setting folder to " + id);
-            mBrowseNode = mBrowseTree.findBrowseNodeByID(id);
-        }
-
         @Override
         public boolean processMessage(Message msg) {
-            Log.d(STATE_TAG, "processMessage " + msg.what);
+            logD(STATE_TAG + " processMessage " + msg.what);
             switch (msg.what) {
                 case MESSAGE_PROCESS_GET_FOLDER_ITEMS:
                     ArrayList<MediaItem> folderList = (ArrayList<MediaItem>) msg.obj;
                     int endIndicator = mBrowseNode.getExpectedChildren() - 1;
-                    if (DBG) {
-                        Log.d(STATE_TAG,
-                                " End " + endIndicator
-                                        + " received " + folderList.size());
-                    }
+                    logD("GetFolderItems: End " + endIndicator
+                            + " received " + folderList.size());
 
                     // Always update the node so that the user does not wait forever
                     // for the list to populate.
                     mBrowseNode.addChildren(folderList);
-                    broadcastFolderList(mBrowseNode.getID());
+                    notifyChanged(mBrowseNode);
 
                     if (mBrowseNode.getChildrenCount() >= endIndicator || folderList.size() == 0
                             || mAbort) {
@@ -505,16 +500,15 @@
                         mBrowseTree.setCurrentBrowsedFolder(BrowseTree.ROOT);
                         rootNode.setExpectedChildren(playerList.size());
                         rootNode.setCached(true);
-                        broadcastFolderList(BrowseTree.ROOT);
+                        notifyChanged(rootNode);
                     }
                     transitionTo(mConnected);
                     break;
 
-
                 case MESSAGE_INTERNAL_CMD_TIMEOUT:
                     // We have timed out to execute the request, we should simply send
                     // whatever listing we have gotten until now.
-                    broadcastFolderList(mBrowseNode.getID());
+                    Log.w(TAG, "TIMEOUT");
                     transitionTo(mConnected);
                     break;
 
@@ -523,41 +517,25 @@
                     // already sent all the items to the client hence simply
                     // transition to Connected state here.
                     mBrowseNode.setCached(true);
-                    broadcastFolderList(mBrowseNode.getID());
                     transitionTo(mConnected);
                     break;
 
-                case MESSAGE_GET_FOLDER_LIST:
-                    if (!mBrowseNode.equals((String) msg.obj)) {
-                        mAbort = true;
+                case MESSAGE_GET_FOLDER_ITEMS:
+                    if (!mBrowseNode.equals(msg.obj)) {
+                        if (mBrowseNode.getScope()
+                                == ((BrowseTree.BrowseNode) msg.obj).getScope()) {
+                            mAbort = true;
+                        }
                         deferMessage(msg);
-                        Log.d(STATE_TAG, "Go Get Another Directory");
+                        logD("GetFolderItems: Go Get Another Directory");
                     } else {
-                        Log.d(STATE_TAG, "Get The Same Directory, ignore");
+                        logD("GetFolderItems: Get The Same Directory, ignore");
                     }
                     break;
 
-                case MESSAGE_FETCH_ATTR_AND_PLAY_ITEM:
-                    // A new request has come in, no need to fetch more.
-                    mAbort = true;
-                    deferMessage(msg);
-                    break;
-
-                case MESSAGE_SEND_PASS_THROUGH_CMD:
-                case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
-                case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
-                case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
-                case MESSAGE_PROCESS_TRACK_CHANGED:
-                case MESSAGE_PROCESS_PLAY_POS_CHANGED:
-                case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
-                case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
-                case MESSAGE_PROCESS_CONNECTION_CHANGE:
-                case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
-                    // All of these messages should be handled by parent state immediately.
-                    return false;
-
                 default:
-                    if (DBG) Log.d(STATE_TAG, "deferring message " + msg.what + " to connected!");
+                    logD(STATE_TAG + " deferring message " + msg.what
+                                + " to connected!");
                     deferMessage(msg);
             }
             return true;
@@ -566,22 +544,23 @@
         private void fetchContents(BrowseTree.BrowseNode target) {
             int start = target.getChildrenCount();
             int end = Math.min(target.getExpectedChildren(), target.getChildrenCount()
-                            + GET_FOLDER_ITEMS_PAGINATION_SIZE) - 1;
+                    + ITEM_PAGE_SIZE) - 1;
             switch (target.getScope()) {
                 case AvrcpControllerService.BROWSE_SCOPE_PLAYER_LIST:
-                    AvrcpControllerService.getPlayerListNative(mRemoteDevice.getBluetoothAddress(),
+                    AvrcpControllerService.getPlayerListNative(mDeviceAddress,
                             start, end);
                     break;
                 case AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING:
                     AvrcpControllerService.getNowPlayingListNative(
-                            mRemoteDevice.getBluetoothAddress(), start, end);
+                            mDeviceAddress, start, end);
                     break;
                 case AvrcpControllerService.BROWSE_SCOPE_VFS:
-                    AvrcpControllerService.getFolderListNative(mRemoteDevice.getBluetoothAddress(),
+                    AvrcpControllerService.getFolderListNative(mDeviceAddress,
                             start, end);
                     break;
                 default:
-                    Log.e(STATE_TAG, "Scope " + target.getScope() + " cannot be handled here.");
+                    Log.e(TAG, STATE_TAG + " Scope " + target.getScope()
+                            + " cannot be handled here.");
             }
         }
 
@@ -598,323 +577,172 @@
          */
         private void navigateToFolderOrRetrieve(BrowseTree.BrowseNode target) {
             mNextStep = mBrowseTree.getNextStepToFolder(target);
-            if (DBG) {
-                Log.d(TAG, "NAVIGATING From " + mBrowseTree.getCurrentBrowsedFolder().toString());
-                Log.d(TAG, "NAVIGATING Toward " + target.toString());
-            }
+            logD("NAVIGATING From "
+                    + mBrowseTree.getCurrentBrowsedFolder().toString());
+            logD("NAVIGATING Toward " + target.toString());
             if (mNextStep == null) {
-                sendMessage(MESSAGE_INTERNAL_CMD_TIMEOUT);
+                return;
             } else if (target.equals(mBrowseTree.mNowPlayingNode)
-                       || target.equals(mBrowseTree.mRootNode)
-                       || mNextStep.equals(mBrowseTree.getCurrentBrowsedFolder())) {
+                    || target.equals(mBrowseTree.mRootNode)
+                    || mNextStep.equals(mBrowseTree.getCurrentBrowsedFolder())) {
                 fetchContents(mNextStep);
             } else if (mNextStep.isPlayer()) {
-                if (DBG) Log.d(TAG, "NAVIGATING Player " + mNextStep.toString());
+                logD("NAVIGATING Player " + mNextStep.toString());
                 if (mNextStep.isBrowsable()) {
                     AvrcpControllerService.setBrowsedPlayerNative(
-                            mRemoteDevice.getBluetoothAddress(), mNextStep.getPlayerID());
+                            mDeviceAddress, (int) mNextStep.getBluetoothID());
                 } else {
-                    if (DBG) Log.d(TAG, "Player doesn't support browsing");
+                    logD("Player doesn't support browsing");
                     mNextStep.setCached(true);
-                    broadcastFolderList(mNextStep.getID());
                     transitionTo(mConnected);
                 }
             } else if (mNextStep.equals(mBrowseTree.mNavigateUpNode)) {
-                if (DBG) Log.d(TAG, "NAVIGATING UP " + mNextStep.toString());
+                logD("NAVIGATING UP " + mNextStep.toString());
                 mNextStep = mBrowseTree.getCurrentBrowsedFolder().getParent();
                 mBrowseTree.getCurrentBrowsedFolder().setCached(false);
 
                 AvrcpControllerService.changeFolderPathNative(
-                        mRemoteDevice.getBluetoothAddress(),
+                        mDeviceAddress,
                         AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP,
-                        AvrcpControllerService.hexStringToByteUID(null));
+                        0);
 
             } else {
-                if (DBG) Log.d(TAG, "NAVIGATING DOWN " + mNextStep.toString());
+                logD("NAVIGATING DOWN " + mNextStep.toString());
                 AvrcpControllerService.changeFolderPathNative(
-                        mRemoteDevice.getBluetoothAddress(),
+                        mDeviceAddress,
                         AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_DOWN,
-                        AvrcpControllerService.hexStringToByteUID(mNextStep.getFolderUID()));
+                        mNextStep.getBluetoothID());
             }
         }
 
         @Override
         public void exit() {
-            mBrowseNode = null;
-            super.exit();
-        }
-    }
-
-    class SetAddresedPlayerAndPlayItem extends CmdState {
-        private static final String STATE_TAG = "AVRCPSM.SetAddresedPlayerAndPlayItem";
-        int mScope;
-        String mPlayItemId;
-        String mAddrPlayerId;
-
-        public void setItemAndScope(String addrPlayerId, String playItemId, int scope) {
-            mAddrPlayerId = addrPlayerId;
-            mPlayItemId = playItemId;
-            mScope = scope;
-        }
-
-        @Override
-        public boolean processMessage(Message msg) {
-            if (DBG) Log.d(STATE_TAG, "processMessage " + msg.what);
-            switch (msg.what) {
-                case MESSAGE_PROCESS_SET_ADDRESSED_PLAYER:
-                    // Set the new addressed player.
-                    mBrowseTree.setCurrentAddressedPlayer(mAddrPlayerId);
-
-                    // And now play the item.
-                    AvrcpControllerService.playItemNative(mRemoteDevice.getBluetoothAddress(),
-                            (byte) mScope, AvrcpControllerService.hexStringToByteUID(mPlayItemId),
-                            (int) 0);
-
-                    // Transition to connected state here.
-                    transitionTo(mConnected);
-                    break;
-
-                case MESSAGE_INTERNAL_CMD_TIMEOUT:
-                    transitionTo(mConnected);
-                    break;
-
-                case MESSAGE_SEND_PASS_THROUGH_CMD:
-                case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
-                case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
-                case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
-                case MESSAGE_PROCESS_TRACK_CHANGED:
-                case MESSAGE_PROCESS_PLAY_POS_CHANGED:
-                case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
-                case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
-                case MESSAGE_PROCESS_CONNECTION_CHANGE:
-                case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
-                    // All of these messages should be handled by parent state immediately.
-                    return false;
-
-                default:
-                    if (DBG) Log.d(STATE_TAG, "deferring message " + msg.what + " to connected!");
-                    deferMessage(msg);
-            }
-            return true;
-        }
-    }
-
-    // Class template for commands. Each state should do the following:
-    // (a) In enter() send a timeout message which could be tracked in the
-    // processMessage() stage.
-    // (b) In exit() remove all the timeouts.
-    //
-    // Essentially the lifecycle of a timeout should be bounded to a CmdState always.
-    abstract class CmdState extends State {
-        @Override
-        public void enter() {
-            sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
-        }
-
-        @Override
-        public void exit() {
             removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT);
+            mBrowseNode = null;
+            super.exit();
         }
     }
 
-    // Interface APIs
-    boolean isConnected() {
-        synchronized (mLock) {
-            return mIsConnected;
+    protected class Disconnecting extends State {
+        @Override
+        public void enter() {
+            onBrowsingDisconnected();
+            broadcastConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTING);
+            transitionTo(mDisconnected);
         }
     }
 
-    void doQuit() {
-        try {
-            mContext.unregisterReceiver(mBroadcastReceiver);
-        } catch (IllegalArgumentException expected) {
-            // If the receiver was never registered unregister will throw an
-            // IllegalArgumentException.
-        }
-        quit();
-    }
-
-    void dump(StringBuilder sb) {
-        if (mRemoteDevice == null) return;
-        BluetoothDevice device = mRemoteDevice.mBTDevice;
-        if (device == null) return;
-        ProfileService.println(sb, "mCurrentDevice: " + device.getAddress() + "("
-                + device.getName() + ") " + this.toString());
-    }
-
-    MediaMetadata getCurrentMetaData() {
-        synchronized (mLock) {
-            if (mAddressedPlayer != null && mAddressedPlayer.getCurrentTrack() != null) {
-                MediaMetadata mmd = mAddressedPlayer.getCurrentTrack().getMediaMetaData();
-                if (DBG) {
-                    Log.d(TAG, "getCurrentMetaData mmd " + mmd);
-                }
-            }
-            return EMPTY_MEDIA_METADATA;
-        }
-    }
-
-    PlaybackState getCurrentPlayBackState() {
-        return getCurrentPlayBackState(true);
-    }
-
-    PlaybackState getCurrentPlayBackState(boolean cached) {
-        if (cached) {
-            synchronized (mLock) {
-                if (mAddressedPlayer == null) {
-                    return new PlaybackState.Builder().setState(PlaybackState.STATE_ERROR,
-                            PlaybackState.PLAYBACK_POSITION_UNKNOWN, 0).build();
-                }
-                return mAddressedPlayer.getPlaybackState();
-            }
-        } else {
-            // Issue a native request, we return NULL since this is only for PTS.
-            AvrcpControllerService.getPlaybackStateNative(mRemoteDevice.getBluetoothAddress());
-            return null;
-        }
-    }
-
-    List<MediaItem> getContents(String uid) {
-        BrowseTree.BrowseNode currentNode = mBrowseTree.findBrowseNodeByID(uid);
-
-        if (DBG) Log.d(TAG, "getContents(" + uid + ") currentNode = " + currentNode);
-        if (currentNode != null) {
-            if (!currentNode.isCached()) {
-                sendMessage(AvrcpControllerStateMachine.MESSAGE_GET_FOLDER_LIST, uid);
-            }
-            return currentNode.getContents();
-        }
-        return null;
-    }
-
-    public void fetchAttrAndPlayItem(String uid) {
-        BrowseTree.BrowseNode currItem = mBrowseTree.findBrowseNodeByID(uid);
-        BrowseTree.BrowseNode currFolder = mBrowseTree.getCurrentBrowsedFolder();
-        if (DBG) Log.d(TAG, "fetchAttrAndPlayItem mediaId=" + uid + " node=" + currItem);
-        if (currItem != null) {
-            int scope = currItem.getParent().isNowPlaying()
-                    ? AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING
-                    : AvrcpControllerService.BROWSE_SCOPE_VFS;
-            Message msg =
-                    obtainMessage(AvrcpControllerStateMachine.MESSAGE_FETCH_ATTR_AND_PLAY_ITEM,
-                            scope, 0, currItem.getFolderUID());
-            sendMessage(msg);
-        }
-    }
-
-    private void broadcastMetaDataChanged(MediaMetadata metadata) {
-        Intent intent = new Intent(AvrcpControllerService.ACTION_TRACK_EVENT);
-        intent.putExtra(AvrcpControllerService.EXTRA_METADATA, metadata);
-        if (VDBG) {
-            Log.d(TAG, " broadcastMetaDataChanged = " + metadata.getDescription());
-        }
-        mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
-
-    }
-
-    private void broadcastFolderList(String id) {
-        Intent intent = new Intent(AvrcpControllerService.ACTION_FOLDER_LIST);
-        if (VDBG) Log.d(TAG, "broadcastFolderList id " + id);
-        intent.putExtra(AvrcpControllerService.EXTRA_FOLDER_ID, id);
-        BluetoothMediaBrowserService bluetoothMediaBrowserService =
-                BluetoothMediaBrowserService.getBluetoothMediaBrowserService();
-        if (bluetoothMediaBrowserService != null) {
-            bluetoothMediaBrowserService.processInternalEvent(intent);
-        }
-    }
-
-    private void broadcastPlayBackStateChanged(PlaybackState state) {
-        Intent intent = new Intent(AvrcpControllerService.ACTION_TRACK_EVENT);
-        intent.putExtra(AvrcpControllerService.EXTRA_PLAYBACK, state);
-        if (DBG) {
-            Log.d(TAG, " broadcastPlayBackStateChanged = " + state.toString());
-        }
-        mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
-    }
 
     private void setAbsVolume(int absVol, int label) {
         int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
         int currIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
-        // Ignore first volume command since phone may not know difference between stream volume
-        // and amplifier volume.
-        if (mRemoteDevice.getFirstAbsVolCmdRecvd()) {
-            int newIndex = (maxVolume * absVol) / ABS_VOL_BASE;
-            if (DBG) {
-                Log.d(TAG, " setAbsVolume =" + absVol + " maxVol = " + maxVolume
-                        + " cur = " + currIndex + " new = " + newIndex);
-            }
-            /*
-             * In some cases change in percentage is not sufficient enough to warrant
-             * change in index values which are in range of 0-15. For such cases
-             * no action is required
-             */
-            if (newIndex != currIndex) {
-                mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newIndex,
-                        AudioManager.FLAG_SHOW_UI);
-            }
-        } else {
-            mRemoteDevice.setFirstAbsVolCmdRecvd();
-            absVol = (currIndex * ABS_VOL_BASE) / maxVolume;
-            if (DBG) Log.d(TAG, " SetAbsVol recvd for first time, respond with " + absVol);
+        int newIndex = (maxVolume * absVol) / ABS_VOL_BASE;
+        logD(" setAbsVolume =" + absVol + " maxVol = " + maxVolume
+                + " cur = " + currIndex + " new = " + newIndex);
+        /*
+         * In some cases change in percentage is not sufficient enough to warrant
+         * change in index values which are in range of 0-15. For such cases
+         * no action is required
+         */
+        if (newIndex != currIndex) {
+            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newIndex,
+                    AudioManager.FLAG_SHOW_UI);
         }
-        AvrcpControllerService.sendAbsVolRspNative(mRemoteDevice.getBluetoothAddress(), absVol,
-                label);
+        AvrcpControllerService.sendAbsVolRspNative(mDeviceAddress, absVol, label);
     }
 
-    private int getVolumePercentage() {
-        int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
-        int currIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
-        int percentageVol = ((currIndex * ABS_VOL_BASE) / maxVolume);
-        return percentageVol;
-    }
-
-    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+    MediaSession.Callback mSessionCallbacks = new MediaSession.Callback() {
         @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) {
-                int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
-                if (streamType == AudioManager.STREAM_MUSIC) {
-                    sendMessage(MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION);
-                }
+        public void onPlay() {
+            logD("onPlay");
+            onPrepare();
+            sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PLAY);
+        }
+
+        @Override
+        public void onPause() {
+            logD("onPause");
+            sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE);
+        }
+
+        @Override
+        public void onSkipToNext() {
+            logD("onSkipToNext");
+            sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_FORWARD);
+        }
+
+        @Override
+        public void onSkipToPrevious() {
+            logD("onSkipToPrevious");
+            sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_BACKWARD);
+        }
+
+        @Override
+        public void onSkipToQueueItem(long id) {
+            logD("onSkipToQueueItem" + id);
+            BrowseTree.BrowseNode node = mBrowseTree.getTrackFromNowPlayingList((int) id);
+            if (node != null) {
+                sendMessage(MESSAGE_PLAY_ITEM, node);
             }
         }
+
+        @Override
+        public void onStop() {
+            logD("onStop");
+            sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_STOP);
+        }
+
+        @Override
+        public void onPrepare() {
+            logD("onPrepare");
+            A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();
+            if (a2dpSinkService != null) {
+                a2dpSinkService.requestAudioFocus(mDevice, true);
+            }
+        }
+
+        @Override
+        public void onRewind() {
+            logD("onRewind");
+            sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_REWIND);
+        }
+
+        @Override
+        public void onFastForward() {
+            logD("onFastForward");
+            sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_FF);
+        }
+
+        @Override
+        public void onPlayFromMediaId(String mediaId, Bundle extras) {
+            // Play the item if possible.
+            onPrepare();
+            BrowseTree.BrowseNode node = mBrowseTree.findBrowseNodeByID(mediaId);
+            Log.w(TAG, "Play Node not found");
+            sendMessage(MESSAGE_PLAY_ITEM, node);
+        }
     };
 
-    public static String dumpMessageString(int message) {
-        String str = "UNKNOWN";
-        switch (message) {
-            case MESSAGE_SEND_PASS_THROUGH_CMD:
-                str = "REQ_PASS_THROUGH_CMD";
-                break;
-            case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
-                str = "REQ_GRP_NAV_CMD";
-                break;
-            case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
-                str = "CB_SET_ABS_VOL_CMD";
-                break;
-            case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
-                str = "CB_REGISTER_ABS_VOL";
-                break;
-            case MESSAGE_PROCESS_TRACK_CHANGED:
-                str = "CB_TRACK_CHANGED";
-                break;
-            case MESSAGE_PROCESS_PLAY_POS_CHANGED:
-                str = "CB_PLAY_POS_CHANGED";
-                break;
-            case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
-                str = "CB_PLAY_STATUS_CHANGED";
-                break;
-            case MESSAGE_PROCESS_RC_FEATURES:
-                str = "CB_RC_FEATURES";
-                break;
-            case MESSAGE_PROCESS_CONNECTION_CHANGE:
-                str = "CB_CONN_CHANGED";
-                break;
-            default:
-                str = Integer.toString(message);
-                break;
+    protected void broadcastConnectionStateChanged(int currentState) {
+        if (mMostRecentState == currentState) {
+            return;
         }
-        return str;
+        if (currentState == BluetoothProfile.STATE_CONNECTED) {
+            MetricsLogger.logProfileConnectionEvent(
+                    BluetoothMetricsProto.ProfileId.AVRCP_CONTROLLER);
+        }
+        logD("Connection state " + mDevice + ": " + mMostRecentState + "->" + currentState);
+        Intent intent = new Intent(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
+        intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, mMostRecentState);
+        intent.putExtra(BluetoothProfile.EXTRA_STATE, currentState);
+        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
+        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+        mMostRecentState = currentState;
+        mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
+    }
+
+    private boolean shouldRequestFocus() {
+        return mService.getResources()
+                .getBoolean(R.bool.a2dp_sink_automatically_request_audio_focus);
     }
 }
diff --git a/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java b/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java
index 8e1af83..bed38d9 100644
--- a/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java
+++ b/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java
@@ -16,7 +16,9 @@
 
 package com.android.bluetooth.avrcpcontroller;
 
+import android.media.MediaMetadata;
 import android.media.session.PlaybackState;
+import android.os.SystemClock;
 import android.util.Log;
 
 import java.util.Arrays;
@@ -26,8 +28,7 @@
  */
 class AvrcpPlayer {
     private static final String TAG = "AvrcpPlayer";
-    private static final boolean DBG = false;
-    private static final boolean VDBG = false;
+    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
 
     public static final int INVALID_ID = -1;
 
@@ -42,19 +43,25 @@
 
     private int mPlayStatus = PlaybackState.STATE_NONE;
     private long mPlayTime = PlaybackState.PLAYBACK_POSITION_UNKNOWN;
+    private long mPlayTimeUpdate = 0;
+    private float mPlaySpeed = 1;
     private int mId;
     private String mName = "";
     private int mPlayerType;
     private byte[] mPlayerFeatures;
     private long mAvailableActions;
-    private TrackInfo mCurrentTrack = new TrackInfo();
+    private MediaMetadata mCurrentTrack;
+    private PlaybackState mPlaybackState;
 
     AvrcpPlayer() {
         mId = INVALID_ID;
         //Set Default Actions in case Player data isn't available.
         mAvailableActions = PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY
-            | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS
-            | PlaybackState.ACTION_STOP;
+                | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS
+                | PlaybackState.ACTION_STOP;
+        PlaybackState.Builder playbackStateBuilder = new PlaybackState.Builder()
+                .setActions(mAvailableActions);
+        mPlaybackState = playbackStateBuilder.build();
     }
 
     AvrcpPlayer(int id, String name, byte[] playerFeatures, int playStatus, int playerType) {
@@ -64,6 +71,9 @@
         mPlayerType = playerType;
         mPlayerFeatures = Arrays.copyOf(playerFeatures, playerFeatures.length);
         updateAvailableActions();
+        PlaybackState.Builder playbackStateBuilder = new PlaybackState.Builder()
+                .setActions(mAvailableActions);
+        mPlaybackState = playbackStateBuilder.build();
     }
 
     public int getId() {
@@ -76,6 +86,9 @@
 
     public void setPlayTime(int playTime) {
         mPlayTime = playTime;
+        mPlayTimeUpdate = SystemClock.elapsedRealtime();
+        mPlaybackState = new PlaybackState.Builder(mPlaybackState).setState(mPlayStatus, mPlayTime,
+                mPlaySpeed).build();
     }
 
     public long getPlayTime() {
@@ -83,7 +96,29 @@
     }
 
     public void setPlayStatus(int playStatus) {
+        mPlayTime += mPlaySpeed * (SystemClock.elapsedRealtime()
+                - mPlaybackState.getLastPositionUpdateTime());
         mPlayStatus = playStatus;
+        switch (mPlayStatus) {
+            case PlaybackState.STATE_STOPPED:
+                mPlaySpeed = 0;
+                break;
+            case PlaybackState.STATE_PLAYING:
+                mPlaySpeed = 1;
+                break;
+            case PlaybackState.STATE_PAUSED:
+                mPlaySpeed = 0;
+                break;
+            case PlaybackState.STATE_FAST_FORWARDING:
+                mPlaySpeed = 3;
+                break;
+            case PlaybackState.STATE_REWINDING:
+                mPlaySpeed = -3;
+                break;
+        }
+
+        mPlaybackState = new PlaybackState.Builder(mPlaybackState).setState(mPlayStatus, mPlayTime,
+                mPlaySpeed).build();
     }
 
     public int getPlayStatus() {
@@ -100,41 +135,19 @@
         if (DBG) {
             Log.d(TAG, "getPlayBackState state " + mPlayStatus + " time " + mPlayTime);
         }
-
-        long position = mPlayTime;
-        float speed = 1;
-        switch (mPlayStatus) {
-            case PlaybackState.STATE_STOPPED:
-                position = 0;
-                speed = 0;
-                break;
-            case PlaybackState.STATE_PAUSED:
-                speed = 0;
-                break;
-            case PlaybackState.STATE_FAST_FORWARDING:
-                speed = 3;
-                break;
-            case PlaybackState.STATE_REWINDING:
-                speed = -3;
-                break;
-        }
-        return new PlaybackState.Builder().setState(mPlayStatus, position, speed)
-            .setActions(mAvailableActions).setActiveQueueItemId(mCurrentTrack.mTrackNum - 1)
-            .build();
+        return mPlaybackState;
     }
 
-    public synchronized boolean updateCurrentTrack(TrackInfo update) {
-        if (update != null && mCurrentTrack != null
-                && update.toString().equals(mCurrentTrack.toString())) {
-            if (DBG) Log.d(TAG, "Update same as original");
-            return false;
+    public synchronized void updateCurrentTrack(MediaMetadata update) {
+        if (update != null) {
+            long trackNumber = update.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER);
+            mPlaybackState = new PlaybackState.Builder(mPlaybackState).setActiveQueueItemId(
+                    trackNumber - 1).build();
         }
-        if (VDBG) Log.d(TAG, "Track Changed Was:" + mCurrentTrack + "now " + update);
         mCurrentTrack = update;
-        return true;
     }
 
-    public synchronized TrackInfo getCurrentTrack() {
+    public synchronized MediaMetadata getCurrentTrack() {
         return mCurrentTrack;
     }
 
diff --git a/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java b/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java
index 4dfb8d4..befe141 100644
--- a/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java
+++ b/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java
@@ -16,39 +16,18 @@
 
 package com.android.bluetooth.avrcpcontroller;
 
-import android.bluetooth.BluetoothAvrcpController;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothProfile;
-import android.content.BroadcastReceiver;
-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;
 import android.media.session.MediaSession;
 import android.media.session.PlaybackState;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
 import android.service.media.MediaBrowserService;
 import android.util.Log;
-import android.util.Pair;
 
 import com.android.bluetooth.R;
-import com.android.bluetooth.a2dpsink.A2dpSinkService;
 
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 /**
  * Implements the MediaBrowserService interface to AVRCP and A2DP
@@ -66,109 +45,15 @@
  */
 public class BluetoothMediaBrowserService extends MediaBrowserService {
     private static final String TAG = "BluetoothMediaBrowserService";
-    private static final boolean DBG = false;
-    private static final boolean VDBG = false;
-
-    private static final String UNKNOWN_BT_AUDIO = "__UNKNOWN_BT_AUDIO__";
-    private static final float PLAYBACK_SPEED = 1.0f;
-
-    // Message sent when A2DP device is disconnected.
-    private static final int MSG_DEVICE_DISCONNECT = 0;
-    // Message sent when A2DP device is connected.
-    private static final int MSG_DEVICE_CONNECT = 2;
-    // Message sent when we recieve a TRACK update from AVRCP profile over a connected A2DP device.
-    private static final int MSG_TRACK = 4;
-    // Internal message sent to trigger a AVRCP action.
-    private static final int MSG_AVRCP_PASSTHRU = 5;
-    // Internal message to trigger a getplaystatus command to remote.
-    private static final int MSG_AVRCP_GET_PLAY_STATUS_NATIVE = 6;
-    // Message sent when AVRCP browse is connected.
-    private static final int MSG_DEVICE_BROWSE_CONNECT = 7;
-    // Message sent when AVRCP browse is disconnected.
-    private static final int MSG_DEVICE_BROWSE_DISCONNECT = 8;
-    // Message sent when folder list is fetched.
-    private static final int MSG_FOLDER_LIST = 9;
-
-    // Custom actions for PTS testing.
-    private static final String CUSTOM_ACTION_VOL_UP =
-            "com.android.bluetooth.avrcpcontroller.CUSTOM_ACTION_VOL_UP";
-    private static final String CUSTOM_ACTION_VOL_DN =
-            "com.android.bluetooth.avrcpcontroller.CUSTOM_ACTION_VOL_DN";
-    private static final String CUSTOM_ACTION_GET_PLAY_STATUS_NATIVE =
-            "com.android.bluetooth.avrcpcontroller.CUSTOM_ACTION_GET_PLAY_STATUS_NATIVE";
+    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
 
     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;
-    private BluetoothDevice mA2dpDevice = null;
-    private A2dpSinkService mA2dpSinkService = null;
-    private Handler mAvrcpCommandQueue;
-    private final Map<String, Result<List<MediaItem>>> mParentIdToRequestMap = new HashMap<>();
-    private int mCurrentlyHeldKey = 0;
 
     // Browsing related structures.
     private List<MediaSession.QueueItem> mMediaQueue = new ArrayList<>();
 
-    private static final class AvrcpCommandQueueHandler extends Handler {
-        WeakReference<BluetoothMediaBrowserService> mInst;
-
-        AvrcpCommandQueueHandler(Looper looper, BluetoothMediaBrowserService sink) {
-            super(looper);
-            mInst = new WeakReference<BluetoothMediaBrowserService>(sink);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            BluetoothMediaBrowserService inst = mInst.get();
-            if (inst == null) {
-                Log.e(TAG, "Parent class has died; aborting.");
-                return;
-            }
-
-            switch (msg.what) {
-                case MSG_DEVICE_CONNECT:
-                    inst.msgDeviceConnect((BluetoothDevice) msg.obj);
-                    break;
-                case MSG_DEVICE_DISCONNECT:
-                    inst.msgDeviceDisconnect((BluetoothDevice) msg.obj);
-                    break;
-                case MSG_TRACK:
-                    Pair<PlaybackState, MediaMetadata> pair =
-                            (Pair<PlaybackState, MediaMetadata>) (msg.obj);
-                    inst.msgTrack(pair.first, pair.second);
-                    break;
-                case MSG_AVRCP_PASSTHRU:
-                    inst.msgPassThru((int) msg.obj);
-                    break;
-                case MSG_AVRCP_GET_PLAY_STATUS_NATIVE:
-                    inst.msgGetPlayStatusNative();
-                    break;
-                case MSG_DEVICE_BROWSE_CONNECT:
-                    inst.msgDeviceBrowseConnect((BluetoothDevice) msg.obj);
-                    break;
-                case MSG_DEVICE_BROWSE_DISCONNECT:
-                    inst.msgDeviceBrowseDisconnect((BluetoothDevice) msg.obj);
-                    break;
-                case MSG_FOLDER_LIST:
-                    inst.msgFolderList((Intent) msg.obj);
-                    break;
-                default:
-                    Log.e(TAG, "Message not handled " + msg);
-            }
-        }
-    }
-
     /**
      * Initialize this BluetoothMediaBrowserService, creating our MediaSession, MediaPlayer and
      * MediaMetaData, and setting up mechanisms to talk with the AvrcpControllerService.
@@ -180,133 +65,34 @@
 
         // Create and configure the MediaSession
         mSession = new MediaSession(this, TAG);
-        mSession.setCallback(mSessionCallbacks);
+        setSessionToken(mSession.getSessionToken());
         mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS
                 | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
         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);
-        filter.addAction(AvrcpControllerService.ACTION_TRACK_EVENT);
-        filter.addAction(AvrcpControllerService.ACTION_FOLDER_LIST);
-        registerReceiver(mBtReceiver, filter);
-
-        synchronized (this) {
-            mParentIdToRequestMap.clear();
-        }
-
-        setBluetoothMediaBrowserService(this);
+        sBluetoothMediaBrowserService = this;
     }
 
-    /**
-     * Clean up this instance in the reverse order that we created it.
-     */
+    List<MediaItem> getContents(final String parentMediaId) {
+        AvrcpControllerService avrcpControllerService =
+                AvrcpControllerService.getAvrcpControllerService();
+        if (avrcpControllerService == null) {
+            return new ArrayList(0);
+        } else {
+            return avrcpControllerService.getContents(parentMediaId);
+        }
+    }
+
     @Override
-    public void onDestroy() {
-        if (DBG) Log.d(TAG, "onDestroy");
-        setBluetoothMediaBrowserService(null);
-        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;
+    public synchronized void onLoadChildren(final String parentMediaId,
+            final Result<List<MediaItem>> result) {
+        if (DBG) Log.d(TAG, "onLoadChildren parentMediaId=" + parentMediaId);
+        List<MediaItem> contents = getContents(parentMediaId);
+        if (contents == null) {
+            result.detach();
+        } else {
+            result.sendResult(contents);
         }
-
-        // 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()
-     *  Routine to get direct access to MediaBrowserService from within the same process.
-     */
-    public static synchronized BluetoothMediaBrowserService getBluetoothMediaBrowserService() {
-        if (sBluetoothMediaBrowserService == null) {
-            Log.w(TAG, "getBluetoothMediaBrowserService(): service is NULL");
-            return null;
-        }
-        if (DBG) {
-            Log.d(TAG, "getBluetoothMediaBrowserService(): returning "
-                    + sBluetoothMediaBrowserService);
-        }
-        return sBluetoothMediaBrowserService;
-    }
-
-    private static synchronized void setBluetoothMediaBrowserService(
-            BluetoothMediaBrowserService instance) {
-        if (DBG) Log.d(TAG, "setBluetoothMediaBrowserService(): set to: " + instance);
-        sBluetoothMediaBrowserService = instance;
     }
 
     @Override
@@ -315,411 +101,83 @@
         return new BrowserRoot(BrowseTree.ROOT, null);
     }
 
-    @Override
-    public synchronized void onLoadChildren(final String parentMediaId,
-            final Result<List<MediaItem>> result) {
-        if (mAvrcpCtrlSrvc == null) {
-            Log.w(TAG, "AVRCP not yet connected.");
-            result.sendResult(Collections.emptyList());
-            return;
-        }
-
-        if (DBG) Log.d(TAG, "onLoadChildren parentMediaId=" + parentMediaId);
-        List<MediaItem> contents = mAvrcpCtrlSrvc.getContents(mA2dpDevice, parentMediaId);
-        if (contents == null) {
-            mParentIdToRequestMap.put(parentMediaId, result);
-            result.detach();
-        } else {
-            result.sendResult(contents);
-        }
-
-        return;
-    }
-
-    @Override
-    public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) {
-    }
-
-    // Media Session Stuff.
-    private MediaSession.Callback mSessionCallbacks = new MediaSession.Callback() {
-        @Override
-        public void onPlay() {
-            if (DBG) Log.d(TAG, "onPlay");
-            mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
-                    AvrcpControllerService.PASS_THRU_CMD_ID_PLAY).sendToTarget();
-            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
-        }
-
-        @Override
-        public void onPause() {
-            if (DBG) Log.d(TAG, "onPause");
-            mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
-                    AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE).sendToTarget();
-            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
-        }
-
-        @Override
-        public void onSkipToNext() {
-            if (DBG) Log.d(TAG, "onSkipToNext");
-            mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
-                    AvrcpControllerService.PASS_THRU_CMD_ID_FORWARD).sendToTarget();
-            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
-        }
-
-        @Override
-        public void onSkipToPrevious() {
-            if (DBG) Log.d(TAG, "onSkipToPrevious");
-            mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
-                    AvrcpControllerService.PASS_THRU_CMD_ID_BACKWARD).sendToTarget();
-            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
-        }
-
-        @Override
-        public void onSkipToQueueItem(long id) {
-            if (DBG) Log.d(TAG, "onSkipToQueueItem" + id);
-            if (mA2dpSinkService != null) {
-                mA2dpSinkService.requestAudioFocus(mA2dpDevice, true);
-            }
-            MediaSession.QueueItem queueItem = mMediaQueue.get((int) id);
-            if (queueItem != null) {
-                String mediaId = queueItem.getDescription().getMediaId();
-                mAvrcpCtrlSrvc.fetchAttrAndPlayItem(mA2dpDevice, mediaId);
-            }
-        }
-
-        @Override
-        public void onStop() {
-            if (DBG) Log.d(TAG, "onStop");
-            mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
-                    AvrcpControllerService.PASS_THRU_CMD_ID_STOP).sendToTarget();
-        }
-
-        @Override
-        public void onPrepare() {
-            if (DBG) Log.d(TAG, "onPrepare");
-            if (mA2dpSinkService != null) {
-                mA2dpSinkService.requestAudioFocus(mA2dpDevice, true);
-                getMediaKeyFocus();
-            }
-        }
-
-        @Override
-        public void onRewind() {
-            if (DBG) Log.d(TAG, "onRewind");
-            mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
-                    AvrcpControllerService.PASS_THRU_CMD_ID_REWIND).sendToTarget();
-            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
-        }
-
-        @Override
-        public void onFastForward() {
-            if (DBG) Log.d(TAG, "onFastForward");
-            mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
-                    AvrcpControllerService.PASS_THRU_CMD_ID_FF).sendToTarget();
-            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
-        }
-
-        @Override
-        public void onPlayFromMediaId(String mediaId, Bundle extras) {
-            synchronized (BluetoothMediaBrowserService.this) {
-                // Play the item if possible.
-                if (mA2dpSinkService != null) {
-                    mA2dpSinkService.requestAudioFocus(mA2dpDevice, true);
-                    getMediaKeyFocus();
-                }
-                mAvrcpCtrlSrvc.fetchAttrAndPlayItem(mA2dpDevice, mediaId);
-            }
-
-            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
-        }
-
-        // Support VOL UP and VOL DOWN events for PTS testing.
-        @Override
-        public void onCustomAction(String action, Bundle extras) {
-            if (DBG) Log.d(TAG, "onCustomAction " + action);
-            if (CUSTOM_ACTION_VOL_UP.equals(action)) {
-                mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
-                        AvrcpControllerService.PASS_THRU_CMD_ID_VOL_UP).sendToTarget();
-            } else if (CUSTOM_ACTION_VOL_DN.equals(action)) {
-                mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
-                        AvrcpControllerService.PASS_THRU_CMD_ID_VOL_DOWN).sendToTarget();
-            } else if (CUSTOM_ACTION_GET_PLAY_STATUS_NATIVE.equals(action)) {
-                mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_GET_PLAY_STATUS_NATIVE).sendToTarget();
-            } else {
-                Log.w(TAG, "Custom action " + action + " not supported.");
-            }
-        }
-    };
-
-    private BroadcastReceiver mBtReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (DBG) Log.d(TAG, "onReceive intent=" + intent);
-            String action = intent.getAction();
-            BluetoothDevice btDev =
-                    (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
-            int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
-
-            if (BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
-                if (DBG) {
-                    Log.d(TAG, "handleConnectionStateChange: newState="
-                            + state + " btDev=" + btDev);
-                }
-
-                // Connected state will be handled when AVRCP BluetoothProfile gets connected.
-                if (state == BluetoothProfile.STATE_CONNECTED) {
-                    mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_CONNECT, btDev).sendToTarget();
-                } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
-                    // Set the playback state to unconnected.
-                    mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_DISCONNECT, btDev).sendToTarget();
-                    // If we have been pushing updates via the session then stop sending them since
-                    // we are not connected anymore.
-                    if (mSession.isActive()) {
-                        mSession.setActive(false);
-                    }
-                }
-            } else if (AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED.equals(
-                    action)) {
-                if (state == BluetoothProfile.STATE_CONNECTED) {
-                    mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_BROWSE_CONNECT, btDev)
-                            .sendToTarget();
-                } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
-                    mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_BROWSE_DISCONNECT, btDev)
-                            .sendToTarget();
-                }
-            } else if (AvrcpControllerService.ACTION_TRACK_EVENT.equals(action)) {
-                PlaybackState pbb =
-                        intent.getParcelableExtra(AvrcpControllerService.EXTRA_PLAYBACK);
-                MediaMetadata mmd =
-                        intent.getParcelableExtra(AvrcpControllerService.EXTRA_METADATA);
-                mAvrcpCommandQueue.obtainMessage(MSG_TRACK,
-                        new Pair<PlaybackState, MediaMetadata>(pbb, mmd)).sendToTarget();
-            } else if (AvrcpControllerService.ACTION_FOLDER_LIST.equals(action)) {
-                mAvrcpCommandQueue.obtainMessage(MSG_FOLDER_LIST, intent).sendToTarget();
-            }
-        }
-    };
-
-    private synchronized void msgDeviceConnect(BluetoothDevice device) {
-        if (DBG) Log.d(TAG, "msgDeviceConnect");
-        // We are connected to a new device via A2DP now.
-        mA2dpDevice = device;
-        mAvrcpCtrlSrvc = AvrcpControllerService.getAvrcpControllerService();
-        if (mAvrcpCtrlSrvc == null) {
-            Log.e(TAG, "!!!AVRCP Controller cannot be null");
-            return;
-        }
-        refreshInitialPlayingState();
-    }
-
-
-    // Refresh the UI if we have a connected device and AVRCP is initialized.
-    private synchronized void refreshInitialPlayingState() {
-        if (mA2dpDevice == null) {
-            if (DBG) Log.d(TAG, "device " + mA2dpDevice);
-            return;
-        }
-
-        List<BluetoothDevice> devices = mAvrcpCtrlSrvc.getConnectedDevices();
-        if (devices.size() == 0) {
-            Log.w(TAG, "No devices connected yet");
-            return;
-        }
-
-        if (mA2dpDevice != null && !mA2dpDevice.equals(devices.get(0))) {
-            Log.w(TAG, "A2dp device : " + mA2dpDevice + " avrcp device " + devices.get(0));
-            return;
-        }
-        mA2dpDevice = devices.get(0);
-        mA2dpSinkService = A2dpSinkService.getA2dpSinkService();
-
-        PlaybackState playbackState = mAvrcpCtrlSrvc.getPlaybackState(mA2dpDevice);
-        MediaMetadata mediaMetadata = mAvrcpCtrlSrvc.getMetaData(mA2dpDevice);
-        if (VDBG) {
-            Log.d(TAG, "Media metadata " + mediaMetadata + " playback state " + playbackState);
-        }
-        mSession.setMetadata(mAvrcpCtrlSrvc.getMetaData(mA2dpDevice));
-        mSession.setPlaybackState(playbackState);
-    }
-
-    private void msgDeviceDisconnect(BluetoothDevice device) {
-        if (DBG) Log.d(TAG, "msgDeviceDisconnect");
-        if (mA2dpDevice == null) {
-            Log.w(TAG, "Already disconnected - nothing to do here.");
-            return;
-        } else if (!mA2dpDevice.equals(device)) {
-            Log.e(TAG,
-                    "Not the right device to disconnect current " + mA2dpDevice + " dc " + device);
-            return;
-        }
-
-        // Unset the session.
-        PlaybackState.Builder pbb = new PlaybackState.Builder();
-        pbb = pbb.setState(PlaybackState.STATE_ERROR, PlaybackState.PLAYBACK_POSITION_UNKNOWN,
-                PLAYBACK_SPEED)
-                .setErrorMessage(getString(R.string.bluetooth_disconnected));
-        mSession.setPlaybackState(pbb.build());
-
-        // Set device to null.
-        mA2dpDevice = null;
-        mBrowseConnected = false;
-        // update playerList.
+    private void updateNowPlayingQueue(BrowseTree.BrowseNode node) {
+        List<MediaItem> songList = node.getContents();
         mMediaQueue.clear();
-        mSession.setQueue(mMediaQueue);
-        notifyChildrenChanged("__ROOT__");
-    }
-
-    private void msgTrack(PlaybackState pb, MediaMetadata mmd) {
-        if (VDBG) Log.d(TAG, "msgTrack: playback: " + pb + " mmd: " + mmd);
-        // Log the current track position/content.
-        MediaController controller = mSession.getController();
-        PlaybackState prevPS = controller.getPlaybackState();
-        MediaMetadata prevMM = controller.getMetadata();
-
-        if (prevPS != null) {
-            Log.d(TAG, "prevPS " + prevPS);
-        }
-
-        if (prevMM != null) {
-            String title = prevMM.getString(MediaMetadata.METADATA_KEY_TITLE);
-            long trackLen = prevMM.getLong(MediaMetadata.METADATA_KEY_DURATION);
-            if (VDBG) Log.d(TAG, "prev MM title " + title + " track len " + trackLen);
-        }
-
-        if (mmd != null) {
-            if (VDBG) Log.d(TAG, "msgTrack() mmd " + mmd.getDescription());
-            mSession.setMetadata(mmd);
-        }
-
-        if (pb != null) {
-            if (DBG) Log.d(TAG, "msgTrack() playbackstate " + pb);
-            mSession.setPlaybackState(pb);
-
-            // If we are now playing then we should start pushing updates via MediaSession so that
-            // external UI (such as SystemUI) can show the currently playing music.
-            if (pb.getState() == PlaybackState.STATE_PLAYING && !mSession.isActive()) {
-                mSession.setActive(true);
+        if (songList != null) {
+            for (MediaItem song : songList) {
+                mMediaQueue.add(new MediaSession.QueueItem(song.getDescription(),
+                        mMediaQueue.size()));
             }
         }
-    }
-
-    private boolean isHoldableKey(int cmd) {
-        return  (cmd == AvrcpControllerService.PASS_THRU_CMD_ID_REWIND)
-                || (cmd == AvrcpControllerService.PASS_THRU_CMD_ID_FF);
-    }
-
-    private synchronized void msgPassThru(int cmd) {
-        if (DBG) Log.d(TAG, "msgPassThru " + cmd);
-        if (mA2dpDevice == null) {
-            // We should have already disconnected - ignore this message.
-            Log.w(TAG, "Already disconnected ignoring.");
-            return;
-        }
-        // Some keys should be held until the next event.
-        if (mCurrentlyHeldKey != 0) {
-            mAvrcpCtrlSrvc.sendPassThroughCmd(mA2dpDevice, mCurrentlyHeldKey,
-                    AvrcpControllerService.KEY_STATE_RELEASED);
-
-            if (mCurrentlyHeldKey == cmd) {
-                // Return to prevent starting FF/FR operation again
-                mCurrentlyHeldKey = 0;
-                return;
-            } else {
-                // FF/FR is in progress and other operation is desired
-                // so after stopping FF/FR, not returning so that command
-                // can be sent for the desired operation.
-                mCurrentlyHeldKey = 0;
-            }
-        }
-
-        // Send the pass through.
-        mAvrcpCtrlSrvc.sendPassThroughCmd(mA2dpDevice, cmd,
-                AvrcpControllerService.KEY_STATE_PRESSED);
-
-        if (isHoldableKey(cmd)) {
-            // Release cmd next time a command is sent.
-            mCurrentlyHeldKey = cmd;
-        } else {
-            mAvrcpCtrlSrvc.sendPassThroughCmd(mA2dpDevice, cmd,
-                    AvrcpControllerService.KEY_STATE_RELEASED);
-        }
-    }
-
-    private synchronized void msgGetPlayStatusNative() {
-        if (DBG) Log.d(TAG, "msgGetPlayStatusNative");
-        if (mA2dpDevice == null) {
-            // We should have already disconnected - ignore this message.
-            Log.w(TAG, "Already disconnected ignoring.");
-            return;
-        }
-
-        // Ask for a non cached version.
-        mAvrcpCtrlSrvc.getPlaybackState(mA2dpDevice, false);
-    }
-
-    private void msgDeviceBrowseConnect(BluetoothDevice device) {
-        if (DBG) Log.d(TAG, "msgDeviceBrowseConnect device " + device);
-        // We should already be connected to this device over A2DP.
-        if (!device.equals(mA2dpDevice)) {
-            Log.e(TAG, "Browse connected over different device a2dp " + mA2dpDevice + " browse "
-                    + device);
-            return;
-        }
-        mBrowseConnected = true;
-        // update playerList
-        notifyChildrenChanged("__ROOT__");
-    }
-
-    private void msgFolderList(Intent intent) {
-        // Parse the folder list for children list and id.
-        String id = intent.getStringExtra(AvrcpControllerService.EXTRA_FOLDER_ID);
-        updateNowPlayingQueue();
-        if (VDBG) Log.d(TAG, "Parent: " + id);
-        synchronized (this) {
-            // If we have a result object then we should send the result back
-            // to client since it is blocking otherwise we may have gotten more items
-            // from remote device, hence let client know to fetch again.
-            Result<List<MediaItem>> results = mParentIdToRequestMap.remove(id);
-            if (results == null) {
-                Log.w(TAG, "Request no longer exists, notifying that children changed.");
-                notifyChildrenChanged(id);
-            } else {
-                List<MediaItem> folderList = mAvrcpCtrlSrvc.getContents(mA2dpDevice, id);
-                results.sendResult(folderList);
-            }
-        }
-    }
-
-    private void updateNowPlayingQueue() {
-        List<MediaItem> songList = mAvrcpCtrlSrvc.getContents(mA2dpDevice, "NOW_PLAYING");
-        Log.d(TAG, "NowPlaying" + songList.size());
-        mMediaQueue.clear();
-        for (MediaItem song : songList) {
-            mMediaQueue.add(new MediaSession.QueueItem(song.getDescription(), mMediaQueue.size()));
-        }
         mSession.setQueue(mMediaQueue);
     }
 
+    static synchronized void notifyChanged(BrowseTree.BrowseNode node) {
+        if (sBluetoothMediaBrowserService != null) {
+            if (node.getScope() == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
+                sBluetoothMediaBrowserService.updateNowPlayingQueue(node);
+            } else {
+                sBluetoothMediaBrowserService.notifyChildrenChanged(node.getID());
+            }
+        }
+    }
+
+    static synchronized void addressedPlayerChanged(MediaSession.Callback callback) {
+        if (sBluetoothMediaBrowserService != null) {
+            sBluetoothMediaBrowserService.mSession.setCallback(callback);
+        } else {
+            Log.w(TAG, "addressedPlayerChanged Unavailable");
+        }
+    }
+
+    static synchronized void trackChanged(MediaMetadata mediaMetadata) {
+        if (sBluetoothMediaBrowserService != null) {
+            sBluetoothMediaBrowserService.mSession.setMetadata(mediaMetadata);
+        } else {
+            Log.w(TAG, "trackChanged Unavailable");
+        }
+    }
+
+    static synchronized void notifyChanged(PlaybackState playbackState) {
+        Log.d(TAG, "notifyChanged PlaybackState" + playbackState);
+        if (sBluetoothMediaBrowserService != null) {
+            sBluetoothMediaBrowserService.mSession.setPlaybackState(playbackState);
+        } else {
+            Log.w(TAG, "notifyChanged Unavailable");
+        }
+    }
+
     /**
-     * processInternalEvent(Intent intent)
-     * Routine to provide MediaBrowserService with content updates from within the same process.
+     * Send AVRCP Play command
      */
-    public void processInternalEvent(Intent intent) {
-        String action = intent.getAction();
-        if (AvrcpControllerService.ACTION_FOLDER_LIST.equals(action)) {
-            mAvrcpCommandQueue.obtainMessage(MSG_FOLDER_LIST, intent).sendToTarget();
+    public static synchronized void play() {
+        if (sBluetoothMediaBrowserService != null) {
+            sBluetoothMediaBrowserService.mSession.getController().getTransportControls().play();
+        } else {
+            Log.w(TAG, "play Unavailable");
         }
     }
 
-    private void msgDeviceBrowseDisconnect(BluetoothDevice device) {
-        if (DBG) Log.d(TAG, "msgDeviceBrowseDisconnect device " + device);
-        // Disconnect only if mA2dpDevice is non null
-        if (!device.equals(mA2dpDevice)) {
-            Log.w(TAG, "Browse disconnecting from different device a2dp " + mA2dpDevice + " browse "
-                    + device);
-            return;
+    /**
+     * Send AVRCP Pause command
+     */
+    public static synchronized void pause() {
+        if (sBluetoothMediaBrowserService != null) {
+            sBluetoothMediaBrowserService.mSession.getController().getTransportControls().pause();
+        } else {
+            Log.w(TAG, "pause Unavailable");
         }
-        mBrowseConnected = false;
     }
 
+    /**
+     * Set Media session active whenever we have Focus of any kind
+     */
+    public static synchronized void setActive(boolean active) {
+        if (sBluetoothMediaBrowserService != null) {
+            sBluetoothMediaBrowserService.mSession.setActive(active);
+        } else {
+            Log.w(TAG, "setActive Unavailable");
+        }
+    }
 }
diff --git a/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java b/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java
index 7f4f3bf..542beee 100644
--- a/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java
+++ b/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java
@@ -16,6 +16,7 @@
 
 package com.android.bluetooth.avrcpcontroller;
 
+import android.bluetooth.BluetoothDevice;
 import android.media.MediaDescription;
 import android.media.browse.MediaBrowser;
 import android.media.browse.MediaBrowser.MediaItem;
@@ -25,6 +26,7 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
+import java.util.UUID;
 
 // Browsing hierarchy.
 // Root:
@@ -39,8 +41,8 @@
 //      ....
 public class BrowseTree {
     private static final String TAG = "BrowseTree";
-    private static final boolean DBG = false;
-    private static final boolean VDBG = false;
+    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
 
     public static final String ROOT = "__ROOT__";
     public static final String UP = "__UP__";
@@ -57,25 +59,28 @@
     final BrowseNode mNavigateUpNode;
     final BrowseNode mNowPlayingNode;
 
-    BrowseTree() {
-        Bundle mdBundle = new Bundle();
-        mdBundle.putString(AvrcpControllerService.MEDIA_ITEM_UID_KEY, ROOT);
-        mRootNode = new BrowseNode(new MediaItem(new MediaDescription.Builder().setExtras(mdBundle)
-              .setMediaId(ROOT).setTitle(ROOT).build(), MediaItem.FLAG_BROWSABLE));
+    BrowseTree(BluetoothDevice device) {
+        if (device == null) {
+            mRootNode = new BrowseNode(new MediaItem(new MediaDescription.Builder()
+                    .setMediaId(ROOT).setTitle(ROOT).build(), MediaItem.FLAG_BROWSABLE));
+            mRootNode.setCached(true);
+        } else {
+            mRootNode = new BrowseNode(new MediaItem(new MediaDescription.Builder()
+                    .setMediaId(ROOT + device.getAddress().toString()).setTitle(
+                            device.getName()).build(), MediaItem.FLAG_BROWSABLE));
+            mRootNode.mDevice = device;
+
+        }
         mRootNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_PLAYER_LIST;
         mRootNode.setExpectedChildren(255);
 
-        Bundle upnodeBundle = new Bundle();
-        upnodeBundle.putString(AvrcpControllerService.MEDIA_ITEM_UID_KEY, UP);
         mNavigateUpNode = new BrowseNode(new MediaItem(new MediaDescription.Builder()
-              .setExtras(upnodeBundle).setMediaId(UP).setTitle(UP).build(),
-              MediaItem.FLAG_BROWSABLE));
+                .setMediaId(UP).setTitle(UP).build(),
+                MediaItem.FLAG_BROWSABLE));
 
-        Bundle nowPlayingBundle = new Bundle();
-        nowPlayingBundle.putString(AvrcpControllerService.MEDIA_ITEM_UID_KEY, NOW_PLAYING_PREFIX);
         mNowPlayingNode = new BrowseNode(new MediaItem(new MediaDescription.Builder()
-              .setExtras(nowPlayingBundle).setMediaId(NOW_PLAYING_PREFIX)
-              .setTitle(NOW_PLAYING_PREFIX).build(), MediaItem.FLAG_BROWSABLE));
+                .setMediaId(NOW_PLAYING_PREFIX)
+                .setTitle(NOW_PLAYING_PREFIX).build(), MediaItem.FLAG_BROWSABLE));
         mNowPlayingNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING;
         mNowPlayingNode.setExpectedChildren(255);
         mBrowseMap.put(ROOT, mRootNode);
@@ -89,11 +94,23 @@
         mBrowseMap.clear();
     }
 
+    void onConnected(BluetoothDevice device) {
+        BrowseNode browseNode = new BrowseNode(device);
+        mRootNode.addChild(browseNode);
+    }
+
+    BrowseNode getTrackFromNowPlayingList(int trackNumber) {
+        return mNowPlayingNode.mChildren.get(trackNumber);
+    }
+
     // Each node of the tree is represented by Folder ID, Folder Name and the children.
     class BrowseNode {
         // MediaItem to store the media related details.
         MediaItem mItem;
 
+        BluetoothDevice mDevice;
+        long mBluetoothId;
+
         // Type of this browse node.
         // Since Media APIs do not define the player separately we define that
         // distinction here.
@@ -103,7 +120,7 @@
         // without doing another fetch.
         boolean mCached = false;
 
-        int mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_VFS;
+        byte mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_VFS;
 
         // List of children.
         private BrowseNode mParent;
@@ -112,6 +129,10 @@
 
         BrowseNode(MediaItem item) {
             mItem = item;
+            Bundle extras = mItem.getDescription().getExtras();
+            if (extras != null) {
+                mBluetoothId = extras.getLong(AvrcpControllerService.MEDIA_ITEM_UID_KEY);
+            }
         }
 
         BrowseNode(AvrcpPlayer player) {
@@ -119,22 +140,29 @@
 
             // Transform the player into a item.
             MediaDescription.Builder mdb = new MediaDescription.Builder();
-            Bundle mdExtra = new Bundle();
             String playerKey = PLAYER_PREFIX + player.getId();
-            mdExtra.putString(AvrcpControllerService.MEDIA_ITEM_UID_KEY, playerKey);
-            mdb.setExtras(mdExtra);
-            mdb.setMediaId(playerKey);
+            mBluetoothId = player.getId();
+
+            mdb.setMediaId(UUID.randomUUID().toString());
             mdb.setTitle(player.getName());
             int mediaItemFlags = player.supportsFeature(AvrcpPlayer.FEATURE_BROWSING)
                     ? MediaBrowser.MediaItem.FLAG_BROWSABLE : 0;
             mItem = new MediaBrowser.MediaItem(mdb.build(), mediaItemFlags);
         }
 
+        BrowseNode(BluetoothDevice device) {
+            boolean mIsPlayer = true;
+            mDevice = device;
+            MediaDescription.Builder mdb = new MediaDescription.Builder();
+            String playerKey = PLAYER_PREFIX + device.getAddress().toString();
+            mdb.setMediaId(playerKey);
+            mdb.setTitle(device.getName());
+            int mediaItemFlags = MediaBrowser.MediaItem.FLAG_BROWSABLE;
+            mItem = new MediaBrowser.MediaItem(mdb.build(), mediaItemFlags);
+        }
+
         private BrowseNode(String name) {
             MediaDescription.Builder mdb = new MediaDescription.Builder();
-            Bundle mdExtra = new Bundle();
-            mdExtra.putString(AvrcpControllerService.MEDIA_ITEM_UID_KEY, name);
-            mdb.setExtras(mdExtra);
             mdb.setMediaId(name);
             mdb.setTitle(name);
             mItem = new MediaBrowser.MediaItem(mdb.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE);
@@ -156,15 +184,32 @@
                 } else if (child instanceof AvrcpPlayer) {
                     currentNode = new BrowseNode((AvrcpPlayer) child);
                 }
-                if (currentNode != null) {
-                    currentNode.mParent = this;
-                    mChildren.add(currentNode);
-                    mBrowseMap.put(currentNode.getID(), currentNode);
-                }
+                addChild(currentNode);
             }
             return newChildren.size();
         }
 
+        synchronized boolean addChild(BrowseNode node) {
+            if (node != null) {
+                node.mParent = this;
+                if (this.mBrowseScope == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
+                    node.mBrowseScope = this.mBrowseScope;
+                }
+                if (node.mDevice == null) {
+                    node.mDevice = this.mDevice;
+                }
+                mChildren.add(node);
+                mBrowseMap.put(node.getID(), node);
+                return true;
+            }
+            return false;
+        }
+
+        synchronized void removeChild(BrowseNode node) {
+            mChildren.remove(node);
+            mBrowseMap.remove(node.getID());
+        }
+
         synchronized int getChildrenCount() {
             return mChildren.size();
         }
@@ -178,7 +223,7 @@
         }
 
         synchronized List<MediaItem> getContents() {
-            if (mChildren != null) {
+            if (mChildren.size() > 0 || mCached) {
                 List<MediaItem> contents = new ArrayList<MediaItem>(mChildren.size());
                 for (BrowseNode child : mChildren) {
                     contents.add(child.getMediaItem());
@@ -221,7 +266,7 @@
             return Integer.parseInt(getID().replace(PLAYER_PREFIX, ""));
         }
 
-        synchronized int getScope() {
+        synchronized byte getScope() {
             return mBrowseScope;
         }
 
@@ -229,9 +274,11 @@
         // This may not be unique hence this combined with direction will define the
         // browsing here.
         synchronized String getFolderUID() {
-            return mItem.getDescription()
-                    .getExtras()
-                    .getString(AvrcpControllerService.MEDIA_ITEM_UID_KEY);
+            return getID();
+        }
+
+        synchronized long getBluetoothID() {
+            return mBluetoothId;
         }
 
         synchronized MediaItem getMediaItem() {
@@ -256,14 +303,20 @@
         }
 
         @Override
-        public String toString() {
+        public synchronized String toString() {
             if (VDBG) {
-                return "[ Name: " + mItem.getDescription().getTitle() + " expected Children: "
+                String serialized = "[ Name: " + mItem.getDescription().getTitle()
+                        + " Scope:" + mBrowseScope + " expected Children: "
                         + mExpectedChildrenCount + "] ";
+                for (BrowseNode node : mChildren) {
+                    serialized += node.toString();
+                }
+                return serialized;
             } else {
                 return "ID: " + getID();
             }
         }
+
         // Returns true if target is a descendant of this.
         synchronized boolean isDescendant(BrowseNode target) {
             return getEldestChild(this, target) == null ? false : true;
@@ -312,6 +365,7 @@
         for (Integer level = 0; level < depth; level++) {
             BrowseNode dummyNode = new BrowseNode(level.toString());
             dummyNode.mParent = mCurrentBrowseNode;
+            dummyNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_VFS;
             mCurrentBrowseNode = dummyNode;
         }
         mCurrentBrowseNode.setExpectedChildren(items);
@@ -342,7 +396,11 @@
 
     @Override
     public String toString() {
-        return "Size: " + mBrowseMap.size();
+        String serialized = "Size: " + mBrowseMap.size();
+        if (VDBG) {
+            serialized += mRootNode.toString();
+        }
+        return serialized;
     }
 
     // Calculates the path to target node.
@@ -373,21 +431,14 @@
                 return nextChild;
             }
         }
-          /*
-          if (mCurrentBrowseNode.isDescendant(target)) {
-            return getEldestChild(mCurrentBrowseNode, target);
-        } else {
-            if (DBG) Log.d(TAG, "NAVIGATING UP");
-            return mNavigateUpNode;
-        }
-        */
     }
 
-    BrowseNode getEldestChild(BrowseNode ancestor, BrowseNode target) {
+    static BrowseNode getEldestChild(BrowseNode ancestor, BrowseNode target) {
         // ancestor is an ancestor of target
         BrowseNode descendant = target;
         if (DBG) {
-            Log.d(TAG, "NAVIGATING ancestor" + ancestor.toString() + "Target" + target.toString());
+            Log.d(TAG, "NAVIGATING ancestor" + ancestor.toString() + "Target"
+                    + target.toString());
         }
         while (!ancestor.equals(descendant.mParent)) {
             descendant = descendant.mParent;
diff --git a/src/com/android/bluetooth/avrcpcontroller/RemoteDevice.java b/src/com/android/bluetooth/avrcpcontroller/RemoteDevice.java
deleted file mode 100644
index 7946747..0000000
--- a/src/com/android/bluetooth/avrcpcontroller/RemoteDevice.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * 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.avrcpcontroller;
-
-import android.bluetooth.BluetoothDevice;
-
-import com.android.bluetooth.Utils;
-
-/*
- * Contains information about remote device specifically the player and features enabled on it along
- * with an encapsulation of the current track and playlist information.
- */
-class RemoteDevice {
-
-    /*
-     * Remote features from JNI
-     */
-    private static final int FEAT_NONE = 0;
-    private static final int FEAT_METADATA = 1;
-    private static final int FEAT_ABSOLUTE_VOLUME = 2;
-    private static final int FEAT_BROWSE = 4;
-
-    private static final int VOLUME_LABEL_UNDEFINED = -1;
-
-    final BluetoothDevice mBTDevice;
-    private int mRemoteFeatures;
-    private boolean mAbsVolNotificationRequested;
-    private boolean mFirstAbsVolCmdRecvd;
-    private int mNotificationLabel;
-
-    RemoteDevice(BluetoothDevice mDevice) {
-        mBTDevice = mDevice;
-        mRemoteFeatures = FEAT_NONE;
-        mAbsVolNotificationRequested = false;
-        mNotificationLabel = VOLUME_LABEL_UNDEFINED;
-        mFirstAbsVolCmdRecvd = false;
-    }
-
-    synchronized void setRemoteFeatures(int remoteFeatures) {
-        mRemoteFeatures = remoteFeatures;
-    }
-
-    public synchronized byte[] getBluetoothAddress() {
-        return Utils.getByteAddress(mBTDevice);
-    }
-
-    public synchronized void setNotificationLabel(int label) {
-        mNotificationLabel = label;
-    }
-
-    public synchronized int getNotificationLabel() {
-        return mNotificationLabel;
-    }
-
-    public synchronized void setAbsVolNotificationRequested(boolean request) {
-        mAbsVolNotificationRequested = request;
-    }
-
-    public synchronized boolean getAbsVolNotificationRequested() {
-        return mAbsVolNotificationRequested;
-    }
-
-    public synchronized void setFirstAbsVolCmdRecvd() {
-        mFirstAbsVolCmdRecvd = true;
-    }
-
-    public synchronized boolean getFirstAbsVolCmdRecvd() {
-        return mFirstAbsVolCmdRecvd;
-    }
-}
diff --git a/src/com/android/bluetooth/avrcpcontroller/StackEvent.java b/src/com/android/bluetooth/avrcpcontroller/StackEvent.java
new file mode 100644
index 0000000..1f96bd2
--- /dev/null
+++ b/src/com/android/bluetooth/avrcpcontroller/StackEvent.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018 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.avrcpcontroller;
+
+final class StackEvent {
+    // Event types for STACK_EVENT message
+    static final int EVENT_TYPE_NONE = 0;
+    static final int EVENT_TYPE_CONNECTION_STATE_CHANGED = 1;
+    static final int EVENT_TYPE_RC_FEATURES = 2;
+
+    int mType = EVENT_TYPE_NONE;
+    boolean mRemoteControlConnected;
+    boolean mBrowsingConnected;
+    int mFeatures;
+
+    private StackEvent(int type) {
+        this.mType = type;
+    }
+
+    @Override
+    public String toString() {
+        switch (mType) {
+            case EVENT_TYPE_CONNECTION_STATE_CHANGED:
+                return "EVENT_TYPE_CONNECTION_STATE_CHANGED " + mRemoteControlConnected;
+            case EVENT_TYPE_RC_FEATURES:
+                return "EVENT_TYPE_RC_FEATURES";
+            default:
+                return "Unknown";
+        }
+    }
+
+    static StackEvent connectionStateChanged(boolean remoteControlConnected,
+            boolean browsingConnected) {
+        StackEvent event = new StackEvent(EVENT_TYPE_CONNECTION_STATE_CHANGED);
+        event.mRemoteControlConnected = remoteControlConnected;
+        event.mBrowsingConnected = browsingConnected;
+        return event;
+    }
+
+    static StackEvent rcFeatures(int features) {
+        StackEvent event = new StackEvent(EVENT_TYPE_RC_FEATURES);
+        event.mFeatures = features;
+        return event;
+    }
+}
diff --git a/src/com/android/bluetooth/avrcpcontroller/TrackInfo.java b/src/com/android/bluetooth/avrcpcontroller/TrackInfo.java
index 7764e60..fd1b784 100644
--- a/src/com/android/bluetooth/avrcpcontroller/TrackInfo.java
+++ b/src/com/android/bluetooth/avrcpcontroller/TrackInfo.java
@@ -14,34 +14,11 @@
  * limitations under the License.
  */
 
-
 package com.android.bluetooth.avrcpcontroller;
 
 import android.media.MediaMetadata;
-import android.util.Log;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/*
- * Contains information about tracks that either currently playing or maintained in playlist
- * This is used as a local repository for information that will be passed on as MediaMetadata to the
- * MediaSessionServicve
- */
-class TrackInfo {
-    private static final String TAG = "AvrcpTrackInfo";
-    private static final boolean VDBG = false;
-
-    /*
-     * Default values for each of the items from JNI
-     */
-    private static final int TRACK_NUM_INVALID = -1;
-    private static final int TOTAL_TRACKS_INVALID = -1;
-    private static final int TOTAL_TRACK_TIME_INVALID = -1;
-    private static final String UNPOPULATED_ATTRIBUTE = "";
-
+final class TrackInfo {
     /*
      *Element Id Values for GetMetaData  from JNI
      */
@@ -53,95 +30,49 @@
     private static final int MEDIA_ATTRIBUTE_GENRE = 0x06;
     private static final int MEDIA_ATTRIBUTE_PLAYING_TIME = 0x07;
 
-
-    private final String mArtistName;
-    private final String mTrackTitle;
-    private final String mAlbumTitle;
-    private final String mGenre;
-    final long mTrackNum; // number of audio file on original recording.
-    private final long mTotalTracks; // total number of tracks on original recording
-    private final long mTrackLen; // full length of AudioFile.
-
-    TrackInfo() {
-        this(new ArrayList<Integer>(), new ArrayList<String>());
-    }
-
-    TrackInfo(List<Integer> attrIds, List<String> attrMap) {
-        Map<Integer, String> attributeMap = new HashMap<>();
-        for (int i = 0; i < attrIds.size(); i++) {
-            attributeMap.put(attrIds.get(i), attrMap.get(i));
+    static MediaMetadata getMetadata(int[] attrIds, String[] attrMap) {
+        MediaMetadata.Builder metaDataBuilder = new MediaMetadata.Builder();
+        int attributeCount = Math.max(attrIds.length, attrMap.length);
+        for (int i = 0; i < attributeCount; i++) {
+            switch (attrIds[i]) {
+                case MEDIA_ATTRIBUTE_TITLE:
+                    metaDataBuilder.putString(MediaMetadata.METADATA_KEY_TITLE, attrMap[i]);
+                    break;
+                case MEDIA_ATTRIBUTE_ARTIST_NAME:
+                    metaDataBuilder.putString(MediaMetadata.METADATA_KEY_ARTIST, attrMap[i]);
+                    break;
+                case MEDIA_ATTRIBUTE_ALBUM_NAME:
+                    metaDataBuilder.putString(MediaMetadata.METADATA_KEY_ALBUM, attrMap[i]);
+                    break;
+                case MEDIA_ATTRIBUTE_TRACK_NUMBER:
+                    try {
+                        metaDataBuilder.putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER,
+                                Long.valueOf(attrMap[i]));
+                    } catch (java.lang.NumberFormatException e) {
+                        // If Track Number doesn't parse, leave it unset
+                    }
+                    break;
+                case MEDIA_ATTRIBUTE_TOTAL_TRACK_NUMBER:
+                    try {
+                        metaDataBuilder.putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS,
+                                Long.valueOf(attrMap[i]));
+                    } catch (java.lang.NumberFormatException e) {
+                        // If Total Track Number doesn't parse, leave it unset
+                    }
+                    break;
+                case MEDIA_ATTRIBUTE_GENRE:
+                    metaDataBuilder.putString(MediaMetadata.METADATA_KEY_GENRE, attrMap[i]);
+                    break;
+                case MEDIA_ATTRIBUTE_PLAYING_TIME:
+                    try {
+                        metaDataBuilder.putLong(MediaMetadata.METADATA_KEY_DURATION,
+                                Long.valueOf(attrMap[i]));
+                    } catch (java.lang.NumberFormatException e) {
+                        // If Playing Time doesn't parse, leave it unset
+                    }
+                    break;
+            }
         }
-
-        String attribute;
-        mTrackTitle = attributeMap.getOrDefault(MEDIA_ATTRIBUTE_TITLE, UNPOPULATED_ATTRIBUTE);
-
-        mArtistName = attributeMap.getOrDefault(MEDIA_ATTRIBUTE_ARTIST_NAME, UNPOPULATED_ATTRIBUTE);
-
-        mAlbumTitle = attributeMap.getOrDefault(MEDIA_ATTRIBUTE_ALBUM_NAME, UNPOPULATED_ATTRIBUTE);
-
-        attribute = attributeMap.get(MEDIA_ATTRIBUTE_TRACK_NUMBER);
-        mTrackNum = (attribute != null && !attribute.isEmpty()) ? Long.valueOf(attribute)
-                : TRACK_NUM_INVALID;
-
-        attribute = attributeMap.get(MEDIA_ATTRIBUTE_TOTAL_TRACK_NUMBER);
-        mTotalTracks = (attribute != null && !attribute.isEmpty()) ? Long.valueOf(attribute)
-                : TOTAL_TRACKS_INVALID;
-
-        mGenre = attributeMap.getOrDefault(MEDIA_ATTRIBUTE_GENRE, UNPOPULATED_ATTRIBUTE);
-
-        attribute = attributeMap.get(MEDIA_ATTRIBUTE_PLAYING_TIME);
-        mTrackLen = (attribute != null && !attribute.isEmpty()) ? Long.valueOf(attribute)
-                : TOTAL_TRACK_TIME_INVALID;
-    }
-
-    @Override
-    public String toString() {
-        return "Metadata [artist=" + mArtistName + " trackTitle= " + mTrackTitle + " albumTitle= "
-                + mAlbumTitle + " genre= " + mGenre + " trackNum= " + Long.toString(mTrackNum)
-                + " track_len : " + Long.toString(mTrackLen) + " TotalTracks " + Long.toString(
-                mTotalTracks) + "]";
-    }
-
-    public MediaMetadata getMediaMetaData() {
-        if (VDBG) {
-            Log.d(TAG, " TrackInfo " + toString());
-        }
-        MediaMetadata.Builder mMetaDataBuilder = new MediaMetadata.Builder();
-        mMetaDataBuilder.putString(MediaMetadata.METADATA_KEY_ARTIST, mArtistName);
-        mMetaDataBuilder.putString(MediaMetadata.METADATA_KEY_TITLE, mTrackTitle);
-        mMetaDataBuilder.putString(MediaMetadata.METADATA_KEY_ALBUM, mAlbumTitle);
-        mMetaDataBuilder.putString(MediaMetadata.METADATA_KEY_GENRE, mGenre);
-        mMetaDataBuilder.putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, mTrackNum);
-        mMetaDataBuilder.putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, mTotalTracks);
-        mMetaDataBuilder.putLong(MediaMetadata.METADATA_KEY_DURATION, mTrackLen);
-        return mMetaDataBuilder.build();
-    }
-
-
-    public String displayMetaData() {
-        MediaMetadata metaData = getMediaMetaData();
-        StringBuffer sb = new StringBuffer();
-        /* getDescription only contains artist, title and album */
-        sb.append(metaData.getDescription().toString() + " ");
-        if (metaData.containsKey(MediaMetadata.METADATA_KEY_GENRE)) {
-            sb.append(metaData.getString(MediaMetadata.METADATA_KEY_GENRE) + " ");
-        }
-        if (metaData.containsKey(MediaMetadata.METADATA_KEY_MEDIA_ID)) {
-            sb.append(metaData.getString(MediaMetadata.METADATA_KEY_MEDIA_ID) + " ");
-        }
-        if (metaData.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) {
-            sb.append(
-                    Long.toString(metaData.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) + " ");
-        }
-        if (metaData.containsKey(MediaMetadata.METADATA_KEY_NUM_TRACKS)) {
-            sb.append(Long.toString(metaData.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS)) + " ");
-        }
-        if (metaData.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) {
-            sb.append(Long.toString(metaData.getLong(MediaMetadata.METADATA_KEY_DURATION)) + " ");
-        }
-        if (metaData.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) {
-            sb.append(Long.toString(metaData.getLong(MediaMetadata.METADATA_KEY_DURATION)) + " ");
-        }
-        return sb.toString();
+        return metaDataBuilder.build();
     }
 }
diff --git a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java
index 1274e48..84d47c7 100644
--- a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java
+++ b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java
@@ -47,7 +47,7 @@
 
     @Mock private Context mMockContext;
 
-    @Mock private A2dpSinkStateMachine mMockA2dpSink;
+    @Mock private A2dpSinkService mMockA2dpSink;
 
     @Mock private AudioManager mMockAudioManager;