Merge d388d5a613ac1228b4abe0ee9f9110aad1b491f9 on remote branch

Change-Id: Iac3de74a3f5a849fae4c4020433cd10b7f0dd068
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index bc10b4b..e435470 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -20,6 +20,7 @@
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.ACCESS_BLUETOOTH_SHARE" />
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.BLUETOOTH" />
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
diff --git a/src/com/android/bluetooth/a2dp/A2dpService.java b/src/com/android/bluetooth/a2dp/A2dpService.java
index fd23784..b232568 100755
--- a/src/com/android/bluetooth/a2dp/A2dpService.java
+++ b/src/com/android/bluetooth/a2dp/A2dpService.java
@@ -72,6 +72,7 @@
     private static A2dpService sA2dpService;
     private static A2dpSinkService sA2dpSinkService;
     private static boolean mA2dpSrcSnkConcurrency;
+    private static boolean a2dpMulticast = false;
 
     private AdapterService mAdapterService;
     private HandlerThread mStateMachinesThread;
@@ -285,6 +286,10 @@
         if (DBG) {
             Log.d(TAG, "A2DP concurrency mode set to " + mA2dpSrcSnkConcurrency);
         }
+        a2dpMulticast = SystemProperties.getBoolean("persist.vendor.service.bt.a2dp_multicast_enable", false);
+        if (DBG) {
+                Log.d(TAG, "A2DP Multicast flag set to " + a2dpMulticast);
+        }
         return true;
     }
 
@@ -978,8 +983,11 @@
                 Log.e(TAG,"adapterService is null");
             }
         }
-        if (mAvrcp_ext != null && !tws_switch) {
-            mAvrcp_ext.setAbsVolumeFlag(device);
+        // Don't update the absVolume flags when disconnect one device in multicast mode
+        if (!a2dpMulticast || previousActiveDevice == null) {
+            if (mAvrcp_ext != null && !tws_switch) {
+                mAvrcp_ext.setAbsVolumeFlag(device);
+            }
         }
         tws_switch = false;
         return true;
@@ -1072,6 +1080,18 @@
         }
     }
 
+    public void storeDeviceAudioVolume(BluetoothDevice device) {
+        if (device != null)
+        {
+            if (AvrcpTargetService.get() != null) {
+                AvrcpTargetService.get().storeVolumeForDevice(device);
+            } else if (mAvrcp_ext != null) {
+                //store volume in multi-a2dp for the device doesn't set as active
+                mAvrcp_ext.storeVolumeForDevice(device);
+            }
+        }
+    }
+
     public void resetAvrcpBlacklist(BluetoothDevice device) {
         synchronized(mBtAvrcpLock) {
             if (mAvrcp_ext != null) {
diff --git a/src/com/android/bluetooth/a2dp/A2dpStateMachine.java b/src/com/android/bluetooth/a2dp/A2dpStateMachine.java
index ceb6a4e..01018d0 100644
--- a/src/com/android/bluetooth/a2dp/A2dpStateMachine.java
+++ b/src/com/android/bluetooth/a2dp/A2dpStateMachine.java
@@ -159,7 +159,6 @@
 
             if (mLastConnectionState != -1) {
                 // Don't broadcast during startup
-                broadcastConnectionState(mConnectionState, mLastConnectionState);
                 if (mIsPlaying) {
                     Log.i(TAG, "Disconnected: stopped playing: " + mDevice);
                     mIsPlaying = false;
@@ -167,6 +166,7 @@
                     broadcastAudioState(BluetoothA2dp.STATE_NOT_PLAYING,
                                         BluetoothA2dp.STATE_PLAYING);
                 }
+                broadcastConnectionState(mConnectionState, mLastConnectionState);
                 AdapterService adapterService = AdapterService.getAdapterService();
                 if (adapterService.isVendorIntfEnabled() &&
                      adapterService.isTwsPlusDevice(mDevice)) {
diff --git a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java
index 3b9056e..d3219e2 100644
--- a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java
+++ b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java
@@ -25,6 +25,7 @@
 import android.media.MediaDescription;
 import android.media.browse.MediaBrowser.MediaItem;
 import android.media.session.PlaybackState;
+import android.os.Message;
 import android.os.Bundle;
 import android.util.Log;
 
@@ -96,11 +97,36 @@
             "android.bluetooth.avrcp-controller.profile.extra.PLAYBACK";
 
 
+    /**
+     * 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";
+
 
     static BrowseTree sBrowseTree;
     private static AvrcpControllerService sService;
     private final BluetoothAdapter mAdapter;
 
+    private int mFeatures;
+    private int mCaPsm;
+
     protected Map<BluetoothDevice, AvrcpControllerStateMachine> mDeviceStateMap =
             new ConcurrentHashMap<>(1);
 
@@ -312,11 +338,15 @@
 
     // Called by JNI to notify Avrcp of features supported by the Remote device.
     private void getRcFeatures(byte[] address, int features, int caPsm) {
-        /*Log.i(TAG, " getRcFeatures caPsm :" + caPsm);
+        Log.i(TAG, " getRcFeatures caPsm :" + caPsm);
+        mFeatures = features;
+        mCaPsm = caPsm;
         BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
-        Message msg = mAvrcpCtSm.obtainMessage(
-            AvrcpControllerStateMachine.MESSAGE_PROCESS_RC_FEATURES, features, caPsm, device);
-        mAvrcpCtSm.sendMessage(msg); */
+        AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+        if (stateMachine != null) {
+            stateMachine.sendMessage(
+                    AvrcpControllerStateMachine.MESSAGE_PROCESS_RC_FEATURES, features, caPsm, device);
+        }
     }
 
     // Called by JNI
@@ -361,15 +391,29 @@
         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<Integer> attrList = new ArrayList<>();
+            for (int attr : attributes) {
+                attrList.add(attr);
+            }
+            List<String> attrValList = Arrays.asList(attribVals);
+            TrackInfo trackInfo = new TrackInfo(attrList, attrValList);
+            stateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_TRACK_CHANGED, trackInfo);
         }
 
     }
 
      private void onElementAttributeUpdate(byte[] address, byte numAttributes, int[] attributes,
             String[] attribVals) {
-
+        if (DBG) {
+            Log.d(TAG, "onElementAttributeUpdate");
+        }
+        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+        AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+        if (stateMachine == null)
+            return;
+        CoverArtUtils coverArtUtils = new CoverArtUtils();
+        coverArtUtils.onElementAttributeUpdate(address, numAttributes, attributes, attribVals,
+                device, stateMachine);
      }
 
     // Called by JNI periodically based upon timer to update play position
diff --git a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java
index e0317f6..03d8f48 100644
--- a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java
+++ b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java
@@ -80,6 +80,7 @@
     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;
+    static final int MESSAGE_PROCESS_RC_FEATURES = 217;
 
     //300->399 Events for Browsing
     static final int MESSAGE_GET_FOLDER_ITEMS = 300;
@@ -112,6 +113,9 @@
     protected final Disconnecting mDisconnecting;
     private A2dpSinkService mA2dpSinkService;
 
+    private static CoverArtUtils mCoveArtUtils;
+    private AvrcpControllerBipStateMachine mBipStateMachine;
+
     protected int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
 
     boolean mRemoteControlConnected = false;
@@ -163,6 +167,9 @@
         IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
         mService.registerReceiver(mBroadcastReceiver, filter);
 
+        mCoveArtUtils = new CoverArtUtils();
+        mBipStateMachine = AvrcpControllerBipStateMachine.make(this, getHandler(), service);
+
         setInitialState(mDisconnected);
     }
 
@@ -356,8 +363,11 @@
                     return true;
 
                 case MESSAGE_PROCESS_TRACK_CHANGED:
-                    mAddressedPlayer.updateCurrentTrack((MediaMetadata) msg.obj);
-                    BluetoothMediaBrowserService.trackChanged((MediaMetadata) msg.obj);
+                    TrackInfo trackInfo = (TrackInfo)msg.obj;
+                    mAddressedPlayer.updateCurrentTrack((MediaMetadata) trackInfo.getMediaMetaData());
+                    BluetoothMediaBrowserService.trackChanged((MediaMetadata) trackInfo.getMediaMetaData());
+                    mAddressedPlayer.updateCurrentTrackInfo(trackInfo);
+                    mCoveArtUtils.msgTrackChanged(mService, mBipStateMachine,mAddressedPlayer,mRemoteDevice);
                     return true;
 
                 case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
@@ -460,6 +470,20 @@
                     mVolumeChangedNotificationsToIgnore = 0;
                     return true;
 
+                case MESSAGE_PROCESS_RC_FEATURES:
+                    mRemoteDevice.setRemoteFeatures(msg.arg1);
+                    if (msg.arg2 > 0) {
+                        mCoveArtUtils.msgProcessRcFeatures(mBipStateMachine, mRemoteDevice,msg.arg2);
+                    }
+                    return true;
+
+                case CoverArtUtils.MESSAGE_BIP_CONNECTED:
+                case CoverArtUtils.MESSAGE_BIP_DISCONNECTED:
+                case CoverArtUtils.MESSAGE_BIP_IMAGE_FETCHED:
+                case CoverArtUtils.MESSAGE_BIP_THUMB_NAIL_FETCHED:
+                    mCoveArtUtils.processBipAction(mService, mAddressedPlayer,
+                            mRemoteDevice, msg.what, msg);
+                    return true;
                 default:
                     return super.processMessage(msg);
             }
diff --git a/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java b/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java
index ab0e987..7bd7bcb 100644
--- a/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java
+++ b/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java
@@ -53,6 +53,8 @@
     private MediaMetadata mCurrentTrack;
     private PlaybackState mPlaybackState;
 
+    private TrackInfo mCurrentTrackInfo = new TrackInfo();
+
     AvrcpPlayer() {
         mId = INVALID_ID;
         //Set Default Actions in case Player data isn't available.
@@ -175,4 +177,12 @@
         }
         if (DBG) Log.d(TAG, "Supported Actions = " + mAvailableActions);
     }
+
+    public synchronized void updateCurrentTrackInfo(TrackInfo update) {
+        mCurrentTrackInfo = update;
+    }
+
+    public synchronized TrackInfo getCurrentTrackInfo() {
+        return mCurrentTrackInfo;
+    }
 }
diff --git a/src/com/android/bluetooth/avrcpcontroller/TrackInfo.java b/src/com/android/bluetooth/avrcpcontroller/TrackInfo.java
index 0240610..961e92e 100644
--- a/src/com/android/bluetooth/avrcpcontroller/TrackInfo.java
+++ b/src/com/android/bluetooth/avrcpcontroller/TrackInfo.java
@@ -17,9 +17,29 @@
 package com.android.bluetooth.avrcpcontroller;
 
 import android.media.MediaMetadata;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.MediaMetadata;
+import android.net.Uri;
+import android.util.Log;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 final class TrackInfo {
+
+    private static final String TAG = "AvrcpTrackInfo";
+    private static final boolean VDBG = AvrcpControllerService.VDBG;
+    /*
+     * 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 = "";
+
     /*
      *Element Id Values for GetMetaData  from JNI
      */
@@ -30,6 +50,108 @@
     private static final int MEDIA_ATTRIBUTE_TOTAL_TRACK_NUMBER = 0x05;
     private static final int MEDIA_ATTRIBUTE_GENRE = 0x06;
     private static final int MEDIA_ATTRIBUTE_PLAYING_TIME = 0x07;
+    private static final int MEDIA_ATTRIBUTE_COVER_ART_HANDLE = 0x08;
+
+    private final String mArtistName;
+    private final String mTrackTitle;
+    private final String mAlbumTitle;
+    private final String mGenre;
+    private 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.
+    private String mCoverArtHandle;
+    private String mImageLocation;
+    private String mThumbNailLocation;
+
+
+    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));
+        }
+
+        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;
+        mCoverArtHandle = attributeMap.getOrDefault(MEDIA_ATTRIBUTE_COVER_ART_HANDLE,
+                UNPOPULATED_ATTRIBUTE);
+        mImageLocation = UNPOPULATED_ATTRIBUTE;
+        mThumbNailLocation = UNPOPULATED_ATTRIBUTE;
+    }
+
+    boolean updateImageLocation(String mCAHandle, String mLocation) {
+        if (VDBG) Log.d(TAG, " updateImageLocation hndl " + mCAHandle + " location " + mLocation);
+        if (!mCAHandle.equals(mCoverArtHandle) || (mLocation == null)) {
+            return false;
+        }
+        mImageLocation = mLocation;
+        return true;
+    }
+
+    boolean updateThumbNailLocation(String mCAHandle, String mLocation) {
+        if (VDBG) Log.d(TAG, " mCAHandle " + mCAHandle + " location " + mLocation);
+        if (!mCAHandle.equals(mCoverArtHandle) || (mLocation == null)) {
+            return false;
+        }
+        mThumbNailLocation = mLocation;
+        return true;
+    }
+
+    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) + " mCoverArtHandle=" + mCoverArtHandle +
+                " mImageLocation :"+mImageLocation+"]";
+    }
+
+
+    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);
+        if (mImageLocation != UNPOPULATED_ATTRIBUTE) {
+            Uri imageUri = Uri.parse(mImageLocation);
+            if (VDBG) Log.d(TAG," updating image uri = " + imageUri.toString());
+            mMetaDataBuilder.putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
+                    imageUri.toString());
+        }
+        if (mThumbNailLocation != UNPOPULATED_ATTRIBUTE) {
+            Bitmap mThumbNailBitmap = BitmapFactory.decodeFile(mThumbNailLocation);
+            mMetaDataBuilder.putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, mThumbNailBitmap);
+        }
+        return mMetaDataBuilder.build();
+    }
+
 
     static MediaMetadata getMetadata(int[] attrIds, String[] attrMap) {
         MediaMetadata.Builder metaDataBuilder = new MediaMetadata.Builder();
@@ -74,7 +196,45 @@
                     break;
             }
         }
-		
+
         return metaDataBuilder.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();
+    }
+
+    String getCoverArtHandle() {
+        return mCoverArtHandle;
+    }
+
+    void clearCoverArtData() {
+        mCoverArtHandle = UNPOPULATED_ATTRIBUTE;
+        mImageLocation = UNPOPULATED_ATTRIBUTE;
+        mThumbNailLocation = UNPOPULATED_ATTRIBUTE;
+    }
+
 }
diff --git a/src/com/android/bluetooth/btservice/ActiveDeviceManager.java b/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
old mode 100644
new mode 100755
index 1a356a8..17b9100
--- a/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
+++ b/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
@@ -33,6 +33,7 @@
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Message;
+import android.os.SystemProperties;
 import android.util.Log;
 
 import com.android.bluetooth.a2dp.A2dpService;
@@ -124,6 +125,8 @@
     private BluetoothDevice mA2dpActiveDevice = null;
     private BluetoothDevice mHfpActiveDevice = null;
     private BluetoothDevice mHearingAidActiveDevice = null;
+    private boolean mTwsPlusSwitch = false;
+    private static boolean a2dpMulticast = false;
 
     // Broadcast receiver for all changes
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@@ -214,8 +217,19 @@
                         mA2dpConnectedDevices.add(device);
                         if (mHearingAidActiveDevice == null) {
                             // New connected device: select it as active
-                            setA2dpActiveDevice(device);
-                            break;
+                            if (!a2dpMulticast) {
+                                setA2dpActiveDevice(device);
+                            }
+                            else {
+                                if (mA2dpActiveDevice == null) {
+                                    setA2dpActiveDevice(device);
+                                }
+                                else {
+                                    // store the volume for the new added device
+                                    final A2dpService a2dpService = mFactory.getA2dpService();
+                                    a2dpService.storeDeviceAudioVolume(device);
+                                }
+                            }
                         }
                         break;
                     }
@@ -231,7 +245,7 @@
                         if (Objects.equals(mA2dpActiveDevice, device)) {
                             final A2dpService mA2dpService = mFactory.getA2dpService();
                             BluetoothDevice mDevice = null;
-                            if (mAdapterService.isTwsPlusDevice(device) &&
+                            if (mAdapterService.isTwsPlusDevice(device) && !mTwsPlusSwitch &&
                                 !mA2dpConnectedDevices.isEmpty()) {
                                 for (BluetoothDevice connected_device: mA2dpConnectedDevices) {
                                     if (mAdapterService.isTwsPlusDevice(connected_device) &&
@@ -241,9 +255,22 @@
                                         break;
                                     }
                                 }
+                            } else if (device.isTwsPlusDevice() && mTwsPlusSwitch) {
+                                Log.d(TAG, "Resetting mTwsPlusSwitch");
+                                mTwsPlusSwitch = false;
+                            }
+                            else if (a2dpMulticast && !mA2dpConnectedDevices.isEmpty()) {
+                                for (BluetoothDevice connected_device: mA2dpConnectedDevices) {
+                                    if (mA2dpService.getConnectionState(connected_device) ==
+                                        BluetoothProfile.STATE_CONNECTED) {
+                                        Log.d(TAG, "a2dp Multicast calling set a2dp Active dev: " + connected_device);
+                                        mDevice = connected_device;
+                                        break;
+                                    }
+                                }
                             }
                             if (!setA2dpActiveDevice(mDevice) && (mDevice != null) &&
-                                mAdapterService.isTwsPlusDevice(mDevice)) {
+                                (mAdapterService.isTwsPlusDevice(mDevice) || a2dpMulticast)) {
                                 Log.w(TAG, "Switch A2dp active device to peer earbud failed");
                                 setA2dpActiveDevice(null);
                             }
@@ -289,7 +316,7 @@
                             break;      // The device is already connected
                         }
                         mHfpConnectedDevices.add(device);
-                        if (mHearingAidActiveDevice == null) {
+                        if ((!a2dpMulticast || mHfpActiveDevice == null) && mHearingAidActiveDevice == null) {
                             // New connected device: select it as active
                             setHfpActiveDevice(device);
                             break;
@@ -329,7 +356,23 @@
                                           "as there is no Connected TWS+ peer");
                                    setHfpActiveDevice(null);
                                 }
-                            } else {
+                            } else if (a2dpMulticast && !mHfpConnectedDevices.isEmpty()) {
+                                if (hfpService == null) {
+                                    Log.e(TAG, "no headsetService, FATAL");
+                                    return;
+                                }
+                                for (BluetoothDevice connected_device: mHfpConnectedDevices) {
+                                    if (hfpService.getConnectionState(connected_device) ==
+                                        BluetoothProfile.STATE_CONNECTED) {
+                                        Log.d(TAG, "a2dp Multicast calling set HFP Active dev: " + connected_device);
+                                        if (!setHfpActiveDevice(connected_device)) {
+                                            setHfpActiveDevice(null);
+                                        }
+                                        break;
+                                    }
+                                }
+                            }
+                            else {
                                setHfpActiveDevice(null);
                             }
                         }
@@ -439,6 +482,7 @@
         mAdapterService.registerReceiver(mReceiver, filter);
 
         mAudioManager.registerAudioDeviceCallback(mAudioManagerAudioDeviceCallback, mHandler);
+        a2dpMulticast = SystemProperties.getBoolean("persist.vendor.service.bt.a2dp_multicast_enable", false);
     }
 
     void cleanup() {
@@ -454,7 +498,12 @@
         }
         resetState();
     }
-
+    public void notify_active_device_unbonding(BluetoothDevice device) {
+        if (device.isTwsPlusDevice() && Objects.equals(mA2dpActiveDevice, device)) {
+            Log.d(TAG,"TWS+ active device is getting unpaired, avoid switch to pair");
+            mTwsPlusSwitch = true;
+        }
+    }
     /**
      * Get the {@link Looper} for the handler thread. This is used in testing and helper
      * objects
diff --git a/src/com/android/bluetooth/btservice/AdapterService.java b/src/com/android/bluetooth/btservice/AdapterService.java
index d9e0eb4..d7c6469 100644
--- a/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/src/com/android/bluetooth/btservice/AdapterService.java
@@ -2472,7 +2472,9 @@
             return false;
         }
         deviceProp.setBondingInitiatedLocally(false);
-
+        if (device.isTwsPlusDevice()) {
+            mActiveDeviceManager.notify_active_device_unbonding(device);
+        }
         Message msg = mBondStateMachine.obtainMessage(BondStateMachine.REMOVE_BOND);
         msg.obj = device;
         mBondStateMachine.sendMessage(msg);
@@ -3598,15 +3600,7 @@
             debugLog(action);
             if (action == null) return;
             if (isEnabled() && (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION))) {
-                 WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
-                 if ((wifiMgr != null) && (wifiMgr.isWifiEnabled())) {
-                     WifiInfo wifiInfo = wifiMgr.getConnectionInfo();
-                     if ((wifiInfo != null) && (wifiInfo.getNetworkId() != -1)) {
-                         mVendor.setWifiState(true);
-                     } else {
-                         mVendor.setWifiState(false);
-                     }
-                 }
+                fetchWifiState();
              } else if (isEnabled() &&
                         (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION) ||
                         (action.equals(WifiManager.WIFI_AP_STATE_CHANGED_ACTION)))){
diff --git a/src/com/android/bluetooth/hfp/HeadsetA2dpSync.java b/src/com/android/bluetooth/hfp/HeadsetA2dpSync.java
index d21d8b6..7fb2e2d 100644
--- a/src/com/android/bluetooth/hfp/HeadsetA2dpSync.java
+++ b/src/com/android/bluetooth/hfp/HeadsetA2dpSync.java
@@ -121,7 +121,8 @@
         if (mHaService != null) {
             HAActiveDevices = mHaService.getActiveDevices();
         }
-        if (HAActiveDevices != null) {
+        if (HAActiveDevices != null && (HAActiveDevices.get(0) != null
+                || HAActiveDevices.get(1) != null)) {
             Log.d(TAG,"Ignore suspendA2DP if active device is HearingAid");
             return false;
         }
@@ -194,7 +195,9 @@
         }
         switch(currState) {
         case BluetoothA2dp.STATE_NOT_PLAYING:
-            mA2dpConnState.put(device, A2DP_CONNECTED);
+            if (mA2dpConnState.containsKey(device)) {
+                mA2dpConnState.put(device, A2DP_CONNECTED);
+            }
             /*
              * send message to statemachine. We send message to SMs
              * only when all devices moved to SUSPENDED.
@@ -206,7 +209,9 @@
             }
             break;
         case BluetoothA2dp.STATE_PLAYING:
-            mA2dpConnState.put(device, A2DP_PLAYING);
+            if (mA2dpConnState.containsKey(device)) {
+                mA2dpConnState.put(device, A2DP_PLAYING);
+            }
             // if call/ ring is ongoing and we received playing,
             // we need to suspend
             if (mHeadsetService.isInCall() || mHeadsetService.isRinging()) {