diff --git a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java index e5353486c23bf..c039fcce5fa8c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java @@ -15,6 +15,8 @@ */ package com.android.settingslib.media; +import android.bluetooth.BluetoothClass; +import android.bluetooth.BluetoothProfile; import android.content.Context; import android.util.Log; @@ -33,6 +35,13 @@ public class BluetoothMediaDevice extends MediaDevice { BluetoothMediaDevice(Context context, CachedBluetoothDevice device) { super(context, MediaDeviceType.TYPE_BLUETOOTH_DEVICE); mCachedDevice = device; + initDeviceRecord(); + buildConnectedState(device); + } + + private void buildConnectedState(CachedBluetoothDevice device) { + mIsConnected = device.isActiveDevice(BluetoothProfile.A2DP) + || device.isActiveDevice(BluetoothProfile.HEARING_AID); } @Override @@ -51,10 +60,16 @@ public class BluetoothMediaDevice extends MediaDevice { return MediaDeviceUtils.getId(mCachedDevice); } + @Override + public void notifyConnectedChanged() { + buildConnectedState(mCachedDevice); + } + @Override public void connect() { //TODO(b/117129183): add callback to notify LocalMediaManager connection state. mIsConnected = mCachedDevice.setActive(); + super.connect(); Log.d(TAG, "connect() device : " + getName() + ", is selected : " + mIsConnected); } @@ -70,4 +85,18 @@ public class BluetoothMediaDevice extends MediaDevice { public CachedBluetoothDevice getCachedDevice() { return mCachedDevice; } + + @Override + protected boolean isCarKitDevice() { + final BluetoothClass bluetoothClass = mCachedDevice.getDevice().getBluetoothClass(); + if (bluetoothClass != null) { + switch (bluetoothClass.getDeviceClass()) { + // Both are common CarKit class + case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE: + case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO: + return true; + } + } + return false; + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaManager.java index 04188e936cf65..a9fc4349cec24 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaManager.java @@ -18,6 +18,7 @@ package com.android.settingslib.media; import android.app.Notification; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; import android.content.Context; import android.util.Log; @@ -30,6 +31,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import java.util.ArrayList; +import java.util.Collection; import java.util.List; /** @@ -39,8 +41,8 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall private static final String TAG = "BluetoothMediaManager"; - private final DeviceAttributeChangeCallback mCachedDeviceCallback = - new DeviceAttributeChangeCallback(); + private final DeviceProfileNotReadyObserverCallback mObserverCallback = + new DeviceProfileNotReadyObserverCallback(); private LocalBluetoothManager mLocalBluetoothManager; private LocalBluetoothProfileManager mProfileManager; @@ -48,6 +50,10 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall private MediaDevice mLastAddedDevice; private MediaDevice mLastRemovedDevice; + private boolean mIsA2dpProfileReady = false; + private boolean mIsHearingAidProfileReady = false; + private Collection mCachedDevices; + BluetoothMediaManager(Context context, LocalBluetoothManager localBluetoothManager, Notification notification) { super(context, notification); @@ -62,6 +68,18 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall mLocalBluetoothManager.getEventManager().registerCallback(this); buildBluetoothDeviceList(); dispatchDeviceListAdded(); + + // The profile may not ready when calling startScan(). + // Device status are all disconnected since profiles are not ready to connected. + // In this case, we observe all devices in CachedDeviceManager. + // When one of these device is connected to profile, will call buildBluetoothDeviceList() + // again to find the connected devices. + if (!mIsA2dpProfileReady && !mIsHearingAidProfileReady) { + mCachedDevices = mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy(); + for (CachedBluetoothDevice device : mCachedDevices) { + device.registerCallback(mObserverCallback); + } + } } private void buildBluetoothDeviceList() { @@ -75,6 +93,7 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall Log.w(TAG, "addConnectedA2dpDevices() a2dp profile is null!"); return; } + final List devices = a2dpProfile.getConnectedDevices(); final CachedBluetoothDeviceManager cachedBluetoothDeviceManager = mLocalBluetoothManager.getCachedDeviceManager(); @@ -95,6 +114,8 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall addMediaDevice(cachedDevice); } } + + mIsA2dpProfileReady = a2dpProfile.isProfileReady(); } private void addConnectedHearingAidDevices() { @@ -103,6 +124,7 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall Log.w(TAG, "addConnectedA2dpDevices() hap profile is null!"); return; } + final List devicesHiSyncIds = new ArrayList<>(); final List devices = hapProfile.getConnectedDevices(); final CachedBluetoothDeviceManager cachedBluetoothDeviceManager = @@ -128,13 +150,14 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall addMediaDevice(cachedDevice); } } + + mIsHearingAidProfileReady = hapProfile.isProfileReady(); } private void addMediaDevice(CachedBluetoothDevice cachedDevice) { MediaDevice mediaDevice = findMediaDevice(MediaDeviceUtils.getId(cachedDevice)); if (mediaDevice == null) { mediaDevice = new BluetoothMediaDevice(mContext, cachedDevice); - cachedDevice.registerCallback(mCachedDeviceCallback); mLastAddedDevice = mediaDevice; mMediaDevices.add(mediaDevice); } @@ -143,16 +166,6 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall @Override public void stopScan() { mLocalBluetoothManager.getEventManager().unregisterCallback(this); - unregisterCachedDeviceCallback(); - } - - private void unregisterCachedDeviceCallback() { - for (MediaDevice device : mMediaDevices) { - if (device instanceof BluetoothMediaDevice) { - ((BluetoothMediaDevice) device).getCachedDevice() - .unregisterCallback(mCachedDeviceCallback); - } - } } @Override @@ -164,8 +177,6 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall final List removeDevicesList = new ArrayList<>(); for (MediaDevice device : mMediaDevices) { if (device instanceof BluetoothMediaDevice) { - ((BluetoothMediaDevice) device).getCachedDevice() - .unregisterCallback(mCachedDeviceCallback); removeDevicesList.add(device); } } @@ -185,7 +196,7 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall private boolean isCachedDeviceConnected(CachedBluetoothDevice cachedDevice) { final boolean isConnectedHearingAidDevice = cachedDevice.isConnectedHearingAidDevice(); final boolean isConnectedA2dpDevice = cachedDevice.isConnectedA2dpDevice(); - Log.d(TAG, "isCachedDeviceConnected() cachedDevice : " + cachedDevice.getName() + Log.d(TAG, "isCachedDeviceConnected() cachedDevice : " + cachedDevice + ", is hearing aid connected : " + isConnectedHearingAidDevice + ", is a2dp connected : " + isConnectedA2dpDevice); @@ -210,7 +221,6 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall private void removeMediaDevice(CachedBluetoothDevice cachedDevice) { final MediaDevice mediaDevice = findMediaDevice(MediaDeviceUtils.getId(cachedDevice)); if (mediaDevice != null) { - cachedDevice.unregisterCallback(mCachedDeviceCallback); mLastRemovedDevice = mediaDevice; mMediaDevices.remove(mediaDevice); } @@ -226,7 +236,7 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall @Override public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, int bluetoothProfile) { - Log.d(TAG, "onProfileConnectionStateChanged() device: " + cachedDevice.getName() + Log.d(TAG, "onProfileConnectionStateChanged() device: " + cachedDevice + ", state: " + state + ", bluetoothProfile: " + bluetoothProfile); if (isCachedDeviceConnected(cachedDevice)) { @@ -240,8 +250,7 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall @Override public void onAclConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { - Log.d(TAG, "onAclConnectionStateChanged() device: " + cachedDevice.getName() - + ", state: " + state); + Log.d(TAG, "onAclConnectionStateChanged() device: " + cachedDevice + ", state: " + state); if (isCachedDeviceConnected(cachedDevice)) { addMediaDevice(cachedDevice); @@ -252,10 +261,29 @@ public class BluetoothMediaManager extends MediaManager implements BluetoothCall } } - class DeviceAttributeChangeCallback implements CachedBluetoothDevice.Callback { + @Override + public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) { + Log.d(TAG, "onActiveDeviceChanged : device : " + + activeDevice + ", profile : " + bluetoothProfile); + if (BluetoothProfile.HEARING_AID == bluetoothProfile + || BluetoothProfile.A2DP == bluetoothProfile) { + final String id = activeDevice == null + ? PhoneMediaDevice.ID : MediaDeviceUtils.getId(activeDevice); + dispatchActiveDeviceChanged(id); + } + } + + class DeviceProfileNotReadyObserverCallback implements CachedBluetoothDevice.Callback { + @Override public void onDeviceAttributesChanged() { - dispatchDeviceAttributesChanged(); + if (!mIsA2dpProfileReady && !mIsHearingAidProfileReady) { + for (CachedBluetoothDevice device : mCachedDevices) { + device.unregisterCallback(mObserverCallback); + } + buildBluetoothDeviceList(); + dispatchDeviceListAdded(); + } } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/ConnectionRecordManager.java b/packages/SettingsLib/src/com/android/settingslib/media/ConnectionRecordManager.java new file mode 100644 index 0000000000000..af343c4980bfe --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/media/ConnectionRecordManager.java @@ -0,0 +1,89 @@ +/* + * Copyright 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.settingslib.media; + +import android.content.Context; +import android.content.SharedPreferences; + +/** + * ConnectionRecordManager represents the sharedPreferences operation on device usage record + */ +public class ConnectionRecordManager { + private static final Object sInstanceSync = new Object(); + private static final String KEY_LAST_SELECTED_DEVICE = "last_selected_device"; + private static final String SHARED_PREFERENCES_NAME = "seamless_transfer_record"; + private static final String TAG = "ConnectionRecordManager"; + private static ConnectionRecordManager sInstance; + + private String mLastSelectedDevice; + + /** + * Get an {@code ConnectionRecordManager} instance (create one if necessary). + */ + public static ConnectionRecordManager getInstance() { + synchronized (sInstanceSync) { + if (sInstance == null) { + sInstance = new ConnectionRecordManager(); + } + } + return sInstance; + } + + private SharedPreferences getSharedPreferences(Context context) { + return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); + } + + /** + * Get connection record from sharedPreferences + * + * @param id a unique device Id + * @return the the usage result + */ + public synchronized int fetchConnectionRecord(Context context, String id) { + return getSharedPreferences(context).getInt(id, 0); + } + + /** + * Get the last selected device from sharedPreferences + */ + public synchronized void fetchLastSelectedDevice(Context context) { + mLastSelectedDevice = getSharedPreferences(context).getString(KEY_LAST_SELECTED_DEVICE, + null); + } + + /** + * Set device usage time and last selected device in sharedPreference + * + * @param id a unique device Id + * @param record usage times + */ + public synchronized void setConnectionRecord(Context context, String id, int record) { + final SharedPreferences.Editor editor = getSharedPreferences(context).edit(); + // Update used times + mLastSelectedDevice = id; + editor.putInt(mLastSelectedDevice, record); + // Update last used device + editor.putString(KEY_LAST_SELECTED_DEVICE, mLastSelectedDevice); + editor.apply(); + } + + /** + * @return the last selected device + */ + public synchronized String getLastSelectedDevice() { + return mLastSelectedDevice; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java index 498a0fc4d8b2a..a2b161fc9d0d6 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java @@ -16,6 +16,7 @@ package com.android.settingslib.media; import android.content.Context; +import android.widget.Toast; import androidx.mediarouter.media.MediaRouter; @@ -33,6 +34,7 @@ public class InfoMediaDevice extends MediaDevice { InfoMediaDevice(Context context, MediaRouter.RouteInfo info) { super(context, MediaDeviceType.TYPE_CAST_DEVICE); mRouteInfo = info; + initDeviceRecord(); } @Override @@ -51,15 +53,23 @@ public class InfoMediaDevice extends MediaDevice { return MediaDeviceUtils.getId(mRouteInfo); } + @Override + public void notifyConnectedChanged() { + //TODO(b/117129183): check mIsConnected state + } + @Override public void connect() { //TODO(b/117129183): use MediaController2 to transfer media mIsConnected = true; + super.connect(); + //mIsConnected = true; + Toast.makeText(mContext, "This is cast device !", Toast.LENGTH_SHORT).show(); } @Override public void disconnect() { //TODO(b/117129183): disconnected last select device - mIsConnected = false; + //mIsConnected = false; } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java index e375ea016ec2b..154834e16cd61 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java @@ -28,15 +28,20 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.List; /** * LocalMediaManager provide interface to get MediaDevice list and transfer media to MediaDevice. */ public class LocalMediaManager implements BluetoothCallback { - + private static final Comparator COMPARATOR = Comparator.naturalOrder(); private static final String TAG = "LocalMediaManager"; + public static final String NOTIFICATION_EXTRA = "notification_extra"; + public static final String NOTIFICATION_PACKAGE_NAME = "notification_package_name"; + @Retention(RetentionPolicy.SOURCE) @IntDef({MediaDeviceState.STATE_CONNECTED, MediaDeviceState.STATE_CONNECTING, @@ -56,7 +61,6 @@ public class LocalMediaManager implements BluetoothCallback { private InfoMediaManager mInfoMediaManager; private LocalBluetoothManager mLocalBluetoothManager; - private MediaDevice mLastConnectedDevice; private MediaDevice mPhoneDevice; /** @@ -96,29 +100,43 @@ public class LocalMediaManager implements BluetoothCallback { * @param connectDevice the MediaDevice */ public void connectDevice(MediaDevice connectDevice) { - if (connectDevice == mLastConnectedDevice) { + final MediaDevice currentDevice = getCurrentConnectedDevice(); + final MediaDevice device = + MediaDeviceUtils.findMediaDevice(mMediaDevices, connectDevice.getId()); + if (device != null && currentDevice != null + && device.getId().equals(currentDevice.getId())) { return; } - if (mLastConnectedDevice != null) { - mLastConnectedDevice.disconnect(); + //TODO(b/117129183): For demo, will remove check connectDevice is InfoMediaDevice. + if (currentDevice != null && !(connectDevice instanceof InfoMediaDevice)) { + currentDevice.disconnect(); } - connectDevice.connect(); - if (connectDevice.isConnected()) { - mLastConnectedDevice = connectDevice; - } + device.connect(); - final int state = connectDevice.isConnected() + final int state = device.isConnected() ? MediaDeviceState.STATE_CONNECTED : MediaDeviceState.STATE_DISCONNECTED; - dispatchSelectedDeviceStateChanged(connectDevice, state); + dispatchSelectedDeviceStateChanged(mMediaDevices, device, state); } - void dispatchSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state) { + private MediaDevice getCurrentConnectedDevice() { + for (MediaDevice device : mMediaDevices) { + if (device.isConnected()) { + return device; + } + } + Log.w(TAG, "getCurrentConnectedDevice() cannot find current connected device !"); + return null; + } + + void dispatchSelectedDeviceStateChanged(List mMediaDevices, MediaDevice device, + @MediaDeviceState int state) { synchronized (mCallbacks) { for (DeviceCallback callback : mCallbacks) { - callback.onSelectedDeviceStateChanged(device, state); + callback.onSelectedDeviceStateChanged(new ArrayList<>(mMediaDevices), device, + state); } } } @@ -153,6 +171,7 @@ public class LocalMediaManager implements BluetoothCallback { void dispatchDeviceListUpdate() { synchronized (mCallbacks) { + Collections.sort(mMediaDevices, COMPARATOR); for (DeviceCallback callback : mCallbacks) { callback.onDeviceListUpdate(new ArrayList<>(mMediaDevices)); } @@ -206,6 +225,25 @@ public class LocalMediaManager implements BluetoothCallback { public void onDeviceAttributesChanged() { dispatchDeviceListUpdate(); } + + @Override + public void onActiveDeviceChanged(String id) { + final MediaDevice currentDevice = getCurrentConnectedDevice(); + final MediaDevice connectDevice = MediaDeviceUtils.findMediaDevice(mMediaDevices, id); + + if (connectDevice != null && currentDevice != null + && connectDevice.getId().equals(currentDevice.getId())) { + return; + } + if (currentDevice != null) { + currentDevice.notifyConnectedChanged(); + } + if (connectDevice != null) { + connectDevice.notifyConnectedChanged(); + } + + dispatchDeviceListUpdate(); + } } @@ -229,6 +267,7 @@ public class LocalMediaManager implements BluetoothCallback { * {@link MediaDeviceState#STATE_CONNECTING}, * {@link MediaDeviceState#STATE_DISCONNECTED} */ - void onSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state); + void onSelectedDeviceStateChanged(List devices, MediaDevice device, + @MediaDeviceState int state); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java index b1c4f14b6aaf3..e4aeda6960307 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java @@ -16,6 +16,8 @@ package com.android.settingslib.media; import android.content.Context; +import android.text.TextUtils; +import android.util.Log; import androidx.annotation.IntDef; @@ -25,20 +27,21 @@ import java.lang.annotation.RetentionPolicy; /** * MediaDevice represents a media device(such like Bluetooth device, cast device and phone device). */ -public abstract class MediaDevice { - +public abstract class MediaDevice implements Comparable { private static final String TAG = "MediaDevice"; @Retention(RetentionPolicy.SOURCE) - @IntDef({MediaDeviceType.TYPE_BLUETOOTH_DEVICE, - MediaDeviceType.TYPE_CAST_DEVICE, + @IntDef({MediaDeviceType.TYPE_CAST_DEVICE, + MediaDeviceType.TYPE_BLUETOOTH_DEVICE, MediaDeviceType.TYPE_PHONE_DEVICE}) public @interface MediaDeviceType { - int TYPE_BLUETOOTH_DEVICE = 1; - int TYPE_CAST_DEVICE = 2; + int TYPE_CAST_DEVICE = 1; + int TYPE_BLUETOOTH_DEVICE = 2; int TYPE_PHONE_DEVICE = 3; } + private int mConnectedRecord; + protected boolean mIsConnected = false; protected Context mContext; protected int mType; @@ -48,6 +51,13 @@ public abstract class MediaDevice { mContext = context; } + void initDeviceRecord() { + ConnectionRecordManager.getInstance().fetchLastSelectedDevice(mContext); + mConnectedRecord = ConnectionRecordManager.getInstance().fetchConnectionRecord(mContext, + getId()); + Log.d("ttttt", getName() + " used: " + mConnectedRecord); + } + /** * Check the MediaDevice is be connected to transfer. * @@ -77,13 +87,79 @@ public abstract class MediaDevice { */ public abstract String getId(); + /** + * Notify MediaDevice to change their connected state. + */ + public abstract void notifyConnectedChanged(); + /** * Transfer MediaDevice for media */ - public abstract void connect(); + public void connect() { + mConnectedRecord++; + ConnectionRecordManager.getInstance().setConnectionRecord(mContext, getId(), + mConnectedRecord); + } /** * Stop transfer MediaDevice */ public abstract void disconnect(); + + /** + * Rules: + * 1. If there is one of the connected devices identified as a carkit, this carkit will + * be always on the top of the device list. Rule 2 and Rule 3 can’t overrule this rule. + * 2. For devices without any usage data yet + * WiFi device group sorted by alphabetical order + BT device group sorted by alphabetical + * order + phone speaker + * 3. For devices with usage record. + * The most recent used one + device group with usage info sorted by how many times the + * device has been used. + * + * So the device list will look like 4 slots ranked as below. + * Rule 1 + the most recently used device + Rule 3 + Rule 2 + * Any slot could be empty. And available device will belong to one of the slots. + * + * @return a negative integer, zero, or a positive integer + * as this object is less than, equal to, or greater than the specified object. + */ + @Override + public int compareTo(MediaDevice another) { + // Check carkit + if (isCarKitDevice()) { + return -1; + } else if (another.isCarKitDevice()) { + return 1; + } + // Set last used device at the first item + String lastSelectedDevice = ConnectionRecordManager.getInstance().getLastSelectedDevice(); + if (TextUtils.equals(lastSelectedDevice, getId())) { + return -1; + } else if (TextUtils.equals(lastSelectedDevice, another.getId())) { + return 1; + } + // Sort by how many times the device has been used if there is usage record + if ((mConnectedRecord != another.mConnectedRecord) + && (another.mConnectedRecord > 0 || mConnectedRecord > 0)) { + return (another.mConnectedRecord - mConnectedRecord); + } + // Both devices have never been used + // To devices with the same type, sort by alphabetical order + if (mType == another.mType) { + final String s1 = getName(); + final String s2 = another.getName(); + return s1.compareToIgnoreCase(s2); + } + // Both devices have never been used, the priority is Cast > Bluetooth > Phone + return mType - another.mType; + } + + /** + * Check if it is CarKit device + * @return true if it is CarKit device + */ + protected boolean isCarKitDevice() { + return false; + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java index 060e9ad3368fc..8066eb0cd7ece 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java @@ -15,15 +15,21 @@ */ package com.android.settingslib.media; +import android.util.Log; + import androidx.mediarouter.media.MediaRouter; import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import java.util.List; + /** * MediaDeviceUtils provides utility function for MediaDevice */ public class MediaDeviceUtils { + private static final String TAG = "MediaDeviceUtils"; + /** * Use CachedBluetoothDevice address to represent unique id * @@ -43,4 +49,21 @@ public class MediaDeviceUtils { public static String getId(MediaRouter.RouteInfo route) { return route.getId(); } + + /** + * Find the MediaDevice through id. + * + * @param devices the list of MediaDevice + * @param id the unique id of MediaDevice + * @return MediaDevice + */ + public static MediaDevice findMediaDevice(List devices, String id) { + for (MediaDevice mediaDevice : devices) { + if (mediaDevice.getId().equals(id)) { + return mediaDevice; + } + } + Log.e(TAG, "findMediaDevice() can't found device"); + return null; + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java index 72b6b09d637ea..ee110702945e4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java @@ -17,7 +17,6 @@ package com.android.settingslib.media; import android.app.Notification; import android.content.Context; -import android.util.Log; import java.util.ArrayList; import java.util.Collection; @@ -68,13 +67,7 @@ public abstract class MediaManager { public abstract void stopScan(); protected MediaDevice findMediaDevice(String id) { - for (MediaDevice mediaDevice : mMediaDevices) { - if (mediaDevice.getId().equals(id)) { - return mediaDevice; - } - } - Log.e(TAG, "findMediaDevice() can't found device"); - return null; + return MediaDeviceUtils.findMediaDevice(mMediaDevices, id); } protected void dispatchDeviceAdded(MediaDevice mediaDevice) { @@ -96,7 +89,7 @@ public abstract class MediaManager { protected void dispatchDeviceListAdded() { synchronized (mCallbacks) { for (MediaDeviceCallback callback : mCallbacks) { - callback.onDeviceListAdded(mMediaDevices); + callback.onDeviceListAdded(new ArrayList<>(mMediaDevices)); } } } @@ -117,6 +110,14 @@ public abstract class MediaManager { } } + protected void dispatchActiveDeviceChanged(String id) { + synchronized (mCallbacks) { + for (MediaDeviceCallback callback : mCallbacks) { + callback.onActiveDeviceChanged(id); + } + } + } + /** * Callback for notifying device is added, removed and attributes changed. */ @@ -153,5 +154,12 @@ public abstract class MediaManager { * Callback for notifying MediaDevice attributes is changed. */ void onDeviceAttributesChanged(); + + /** + * Callback for notifying active MediaDevice is changed. + * + * @param id the id of MediaDevice + */ + void onActiveDeviceChanged(String id); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java index 5e49d6b49e117..59150f1941a8c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java @@ -15,6 +15,7 @@ */ package com.android.settingslib.media; +import android.bluetooth.BluetoothDevice; import android.content.Context; import android.util.Log; @@ -24,6 +25,8 @@ import com.android.settingslib.bluetooth.HearingAidProfile; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import java.util.List; + /** * PhoneMediaDevice extends MediaDevice to represents Phone device. */ @@ -41,6 +44,30 @@ public class PhoneMediaDevice extends MediaDevice { mLocalBluetoothManager = localBluetoothManager; mProfileManager = mLocalBluetoothManager.getProfileManager(); + initDeviceRecord(); + + mIsConnected = isPhoneActive(); + } + + private boolean isPhoneActive() { + boolean isActive = true; + + final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile(); + final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); + + if (a2dpProfile.getActiveDevice() == null) { + final List activeDevices = hapProfile.getActiveDevices(); + for (BluetoothDevice btDevice : activeDevices) { + if (btDevice != null) { + isActive = false; + break; + } + } + } else { + isActive = false; + } + + return isActive; } @Override @@ -60,14 +87,19 @@ public class PhoneMediaDevice extends MediaDevice { return ID; } + @Override + public void notifyConnectedChanged() { + mIsConnected = isPhoneActive(); + } + @Override public void connect() { final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile(); final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); if (hapProfile != null && a2dpProfile != null) { - mIsConnected = - hapProfile.setActiveDevice(null) && a2dpProfile.setActiveDevice(null); + mIsConnected = hapProfile.setActiveDevice(null) && a2dpProfile.setActiveDevice(null); + super.connect(); } Log.d(TAG, "connect() device : " + getName() + ", is selected : " + mIsConnected); } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java new file mode 100644 index 0000000000000..1cbf277caf8db --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java @@ -0,0 +1,270 @@ +/* + * Copyright 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.settingslib.media; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothClass; +import android.bluetooth.BluetoothDevice; +import android.content.Context; + +import androidx.mediarouter.media.MediaRouter; + +import com.android.settingslib.bluetooth.A2dpProfile; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class MediaDeviceTest { + private static final Comparator COMPARATOR = Comparator.naturalOrder(); + private static final String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11"; + private static final String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22"; + private static final String DEVICE_ADDRESS_3 = "AA:BB:CC:DD:EE:33"; + private static final String DEVICE_NAME_1 = "TestName_1"; + private static final String DEVICE_NAME_2 = "TestName_2"; + private static final String DEVICE_NAME_3 = "TestName_3"; + private static final String ROUTER_ID_1 = "RouterId_1"; + private static final String ROUTER_ID_2 = "RouterId_2"; + private static final String ROUTER_ID_3 = "RouterId_3"; + private final BluetoothClass mHeadreeClass = + new BluetoothClass(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES); + private final BluetoothClass mCarkitClass = + new BluetoothClass(BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO); + + @Mock + private A2dpProfile mA2dpProfile; + @Mock + private BluetoothDevice mDevice1; + @Mock + private BluetoothDevice mDevice2; + @Mock + private BluetoothDevice mDevice3; + @Mock + private CachedBluetoothDevice mCachedDevice1; + @Mock + private CachedBluetoothDevice mCachedDevice2; + @Mock + private CachedBluetoothDevice mCachedDevice3; + @Mock + private LocalBluetoothManager mLocalBluetoothManager; + @Mock + private LocalBluetoothProfileManager mProfileManager; + @Mock + private MediaRouter.RouteInfo mRouteInfo1; + @Mock + private MediaRouter.RouteInfo mRouteInfo2; + @Mock + private MediaRouter.RouteInfo mRouteInfo3; + + private BluetoothMediaDevice mBluetoothMediaDevice1; + private BluetoothMediaDevice mBluetoothMediaDevice2; + private BluetoothMediaDevice mBluetoothMediaDevice3; + private Context mContext; + private InfoMediaDevice mInfoMediaDevice1; + private InfoMediaDevice mInfoMediaDevice2; + private InfoMediaDevice mInfoMediaDevice3; + private List mMediaDevices = new ArrayList<>(); + private PhoneMediaDevice mPhoneMediaDevice; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + + when(mCachedDevice1.getAddress()).thenReturn(DEVICE_ADDRESS_1); + when(mCachedDevice2.getAddress()).thenReturn(DEVICE_ADDRESS_2); + when(mCachedDevice3.getAddress()).thenReturn(DEVICE_ADDRESS_3); + when(mCachedDevice1.getName()).thenReturn(DEVICE_NAME_1); + when(mCachedDevice2.getName()).thenReturn(DEVICE_NAME_2); + when(mCachedDevice3.getName()).thenReturn(DEVICE_NAME_3); + when(mCachedDevice1.getDevice()).thenReturn(mDevice1); + when(mCachedDevice2.getDevice()).thenReturn(mDevice2); + when(mCachedDevice3.getDevice()).thenReturn(mDevice3); + when(mRouteInfo1.getId()).thenReturn(ROUTER_ID_1); + when(mRouteInfo2.getId()).thenReturn(ROUTER_ID_2); + when(mRouteInfo3.getId()).thenReturn(ROUTER_ID_3); + when(mRouteInfo1.getName()).thenReturn(DEVICE_NAME_1); + when(mRouteInfo2.getName()).thenReturn(DEVICE_NAME_2); + when(mRouteInfo3.getName()).thenReturn(DEVICE_NAME_3); + when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager); + when(mProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile); + when(mA2dpProfile.getActiveDevice()).thenReturn(mDevice1); + + mBluetoothMediaDevice1 = new BluetoothMediaDevice(mContext, mCachedDevice1); + mBluetoothMediaDevice2 = new BluetoothMediaDevice(mContext, mCachedDevice2); + mBluetoothMediaDevice3 = new BluetoothMediaDevice(mContext, mCachedDevice3); + mInfoMediaDevice1 = new InfoMediaDevice(mContext, mRouteInfo1); + mInfoMediaDevice2 = new InfoMediaDevice(mContext, mRouteInfo2); + mInfoMediaDevice3 = new InfoMediaDevice(mContext, mRouteInfo3); + mPhoneMediaDevice = new PhoneMediaDevice(mContext, mLocalBluetoothManager); + } + + @Test + public void compareTo_carKit_nonCarKitBluetooth_carKitFirst() { + when(mDevice1.getBluetoothClass()).thenReturn(mHeadreeClass); + when(mDevice2.getBluetoothClass()).thenReturn(mCarkitClass); + mMediaDevices.add(mBluetoothMediaDevice1); + mMediaDevices.add(mBluetoothMediaDevice2); + + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1); + Collections.sort(mMediaDevices, COMPARATOR); + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice2); + } + + @Test + public void compareTo_carKit_info_carKitFirst() { + when(mDevice1.getBluetoothClass()).thenReturn(mCarkitClass); + mMediaDevices.add(mInfoMediaDevice1); + mMediaDevices.add(mBluetoothMediaDevice1); + + assertThat(mMediaDevices.get(0)).isEqualTo(mInfoMediaDevice1); + Collections.sort(mMediaDevices, COMPARATOR); + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1); + } + + @Test + public void compareTo_carKit_phone_carKitFirst() { + when(mDevice1.getBluetoothClass()).thenReturn(mCarkitClass); + mMediaDevices.add(mPhoneMediaDevice); + mMediaDevices.add(mBluetoothMediaDevice1); + + assertThat(mMediaDevices.get(0)).isEqualTo(mPhoneMediaDevice); + Collections.sort(mMediaDevices, COMPARATOR); + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1); + } + + @Test + public void compareTo_lastSelected_others_lastSelectedFirst() { + mMediaDevices.add(mBluetoothMediaDevice1); + mMediaDevices.add(mBluetoothMediaDevice2); + mBluetoothMediaDevice2.connect(); + + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1); + Collections.sort(mMediaDevices, COMPARATOR); + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice2); + } + @Test + public void compareTo_connectionRecord_sortByRecord() { + mMediaDevices.add(mBluetoothMediaDevice1); + mMediaDevices.add(mBluetoothMediaDevice2); + mBluetoothMediaDevice1.connect(); + mBluetoothMediaDevice2.connect(); + mBluetoothMediaDevice2.connect(); + // Reset last selected record + ConnectionRecordManager.getInstance().setConnectionRecord(mContext, null, 0); + + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1); + assertThat(mMediaDevices.get(1)).isEqualTo(mBluetoothMediaDevice2); + Collections.sort(mMediaDevices, COMPARATOR); + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice2); + assertThat(mMediaDevices.get(1)).isEqualTo(mBluetoothMediaDevice1); + } + + @Test + public void compareTo_info_bluetooth_infoFirst() { + mMediaDevices.add(mBluetoothMediaDevice1); + mMediaDevices.add(mInfoMediaDevice1); + + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1); + Collections.sort(mMediaDevices, COMPARATOR); + assertThat(mMediaDevices.get(0)).isEqualTo(mInfoMediaDevice1); + } + + @Test + public void compareTo_bluetooth_phone_bluetoothFirst() { + mMediaDevices.add(mPhoneMediaDevice); + mMediaDevices.add(mBluetoothMediaDevice1); + + assertThat(mMediaDevices.get(0)).isEqualTo(mPhoneMediaDevice); + Collections.sort(mMediaDevices, COMPARATOR); + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1); + } + + @Test + public void compareTo_twoInfo_sortByAlphabet() { + mMediaDevices.add(mInfoMediaDevice2); + mMediaDevices.add(mInfoMediaDevice1); + + assertThat(mMediaDevices.get(0)).isEqualTo(mInfoMediaDevice2); + Collections.sort(mMediaDevices, COMPARATOR); + assertThat(mMediaDevices.get(0)).isEqualTo(mInfoMediaDevice1); + } + + @Test + public void compareTo_twoBluetooth_sortByAlphabet() { + mMediaDevices.add(mBluetoothMediaDevice2); + mMediaDevices.add(mBluetoothMediaDevice1); + + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice2); + Collections.sort(mMediaDevices, COMPARATOR); + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1); + } + + // 1.mInfoMediaDevice1: Last Selected device + // 2.mBluetoothMediaDevice1: CarKit device + // 3.mInfoMediaDevice2: * 2 times usage + // 4.mInfoMediaDevice3: * 1 time usage + // 5.mBluetoothMediaDevice2: * 2 times usage + // 6.mBluetoothMediaDevice3: * 1 time usage + // 7.mPhoneMediaDevice: * 0 time usage + // Order: 2 -> 1 -> 3 -> 5 -> 4 -> 6 -> 7 + @Test + public void compareTo_mixedDevices_carKitFirst() { + when(mDevice1.getBluetoothClass()).thenReturn(mCarkitClass); + when(mDevice2.getBluetoothClass()).thenReturn(mHeadreeClass); + when(mDevice3.getBluetoothClass()).thenReturn(mHeadreeClass); + mMediaDevices.add(mBluetoothMediaDevice1); + mMediaDevices.add(mBluetoothMediaDevice2); + mMediaDevices.add(mBluetoothMediaDevice3); + mMediaDevices.add(mInfoMediaDevice1); + mMediaDevices.add(mInfoMediaDevice2); + mMediaDevices.add(mInfoMediaDevice3); + mMediaDevices.add(mPhoneMediaDevice); + mBluetoothMediaDevice3.connect(); + mBluetoothMediaDevice2.connect(); + mBluetoothMediaDevice2.connect(); + mInfoMediaDevice3.connect(); + mInfoMediaDevice2.connect(); + mInfoMediaDevice2.connect(); + mInfoMediaDevice1.connect(); + + Collections.sort(mMediaDevices, COMPARATOR); + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1); + assertThat(mMediaDevices.get(1)).isEqualTo(mInfoMediaDevice1); + assertThat(mMediaDevices.get(2)).isEqualTo(mInfoMediaDevice2); + assertThat(mMediaDevices.get(3)).isEqualTo(mBluetoothMediaDevice2); + assertThat(mMediaDevices.get(4)).isEqualTo(mInfoMediaDevice3); + assertThat(mMediaDevices.get(5)).isEqualTo(mBluetoothMediaDevice3); + assertThat(mMediaDevices.get(6)).isEqualTo(mPhoneMediaDevice); + } + +}