1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.settingslib.media; 17 18 import android.app.Notification; 19 import android.bluetooth.BluetoothProfile; 20 import android.content.Context; 21 import android.util.Log; 22 23 import androidx.annotation.IntDef; 24 25 import com.android.internal.annotations.VisibleForTesting; 26 import com.android.settingslib.bluetooth.BluetoothCallback; 27 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 28 import com.android.settingslib.bluetooth.LocalBluetoothManager; 29 30 import java.lang.annotation.Retention; 31 import java.lang.annotation.RetentionPolicy; 32 import java.util.ArrayList; 33 import java.util.Collection; 34 import java.util.Collections; 35 import java.util.Comparator; 36 import java.util.List; 37 38 /** 39 * LocalMediaManager provide interface to get MediaDevice list and transfer media to MediaDevice. 40 */ 41 public class LocalMediaManager implements BluetoothCallback { 42 private static final Comparator<MediaDevice> COMPARATOR = Comparator.naturalOrder(); 43 private static final String TAG = "LocalMediaManager"; 44 45 @Retention(RetentionPolicy.SOURCE) 46 @IntDef({MediaDeviceState.STATE_CONNECTED, 47 MediaDeviceState.STATE_CONNECTING, 48 MediaDeviceState.STATE_DISCONNECTED}) 49 public @interface MediaDeviceState { 50 int STATE_CONNECTED = 1; 51 int STATE_CONNECTING = 2; 52 int STATE_DISCONNECTED = 3; 53 } 54 55 private final Collection<DeviceCallback> mCallbacks = new ArrayList<>(); 56 @VisibleForTesting 57 final MediaDeviceCallback mMediaDeviceCallback = new MediaDeviceCallback(); 58 59 private Context mContext; 60 private BluetoothMediaManager mBluetoothMediaManager; 61 private LocalBluetoothManager mLocalBluetoothManager; 62 63 @VisibleForTesting 64 List<MediaDevice> mMediaDevices = new ArrayList<>(); 65 @VisibleForTesting 66 MediaDevice mPhoneDevice; 67 @VisibleForTesting 68 MediaDevice mCurrentConnectedDevice; 69 70 /** 71 * Register to start receiving callbacks for MediaDevice events. 72 */ 73 public void registerCallback(DeviceCallback callback) { 74 synchronized (mCallbacks) { 75 mCallbacks.add(callback); 76 } 77 } 78 79 /** 80 * Unregister to stop receiving callbacks for MediaDevice events 81 */ 82 public void unregisterCallback(DeviceCallback callback) { 83 synchronized (mCallbacks) { 84 mCallbacks.remove(callback); 85 } 86 } 87 88 public LocalMediaManager(Context context, String packageName, Notification notification) { 89 mContext = context; 90 mLocalBluetoothManager = 91 LocalBluetoothManager.getInstance(context, /* onInitCallback= */ null); 92 if (mLocalBluetoothManager == null) { 93 Log.e(TAG, "Bluetooth is not supported on this device"); 94 return; 95 } 96 97 mBluetoothMediaManager = 98 new BluetoothMediaManager(context, mLocalBluetoothManager, notification); 99 } 100 101 @VisibleForTesting 102 LocalMediaManager(Context context, LocalBluetoothManager localBluetoothManager, 103 BluetoothMediaManager bluetoothMediaManager, InfoMediaManager infoMediaManager) { 104 mContext = context; 105 mLocalBluetoothManager = localBluetoothManager; 106 mBluetoothMediaManager = bluetoothMediaManager; 107 } 108 109 /** 110 * Connect the MediaDevice to transfer media 111 * @param connectDevice the MediaDevice 112 */ 113 public void connectDevice(MediaDevice connectDevice) { 114 final MediaDevice device = getMediaDeviceById(mMediaDevices, connectDevice.getId()); 115 if (device instanceof BluetoothMediaDevice) { 116 final CachedBluetoothDevice cachedDevice = 117 ((BluetoothMediaDevice) device).getCachedDevice(); 118 if (!cachedDevice.isConnected() && !cachedDevice.isBusy()) { 119 cachedDevice.connect(true); 120 return; 121 } 122 } 123 124 if (device == mCurrentConnectedDevice) { 125 Log.d(TAG, "connectDevice() this device all ready connected! : " + device.getName()); 126 return; 127 } 128 129 //TODO(b/121083246): Update it once remote media API is ready. 130 if (mCurrentConnectedDevice != null && !(connectDevice instanceof InfoMediaDevice)) { 131 mCurrentConnectedDevice.disconnect(); 132 } 133 134 final boolean isConnected = device.connect(); 135 if (isConnected) { 136 mCurrentConnectedDevice = device; 137 } 138 139 final int state = isConnected 140 ? MediaDeviceState.STATE_CONNECTED 141 : MediaDeviceState.STATE_DISCONNECTED; 142 dispatchSelectedDeviceStateChanged(device, state); 143 } 144 145 void dispatchSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state) { 146 synchronized (mCallbacks) { 147 for (DeviceCallback callback : mCallbacks) { 148 callback.onSelectedDeviceStateChanged(device, state); 149 } 150 } 151 } 152 153 /** 154 * Start scan connected MediaDevice 155 */ 156 public void startScan() { 157 mMediaDevices.clear(); 158 mBluetoothMediaManager.registerCallback(mMediaDeviceCallback); 159 mBluetoothMediaManager.startScan(); 160 } 161 162 private void addPhoneDeviceIfNecessary() { 163 // add phone device to list if there have any Bluetooth device and cast device. 164 if (mMediaDevices.size() > 0 && !mMediaDevices.contains(mPhoneDevice)) { 165 if (mPhoneDevice == null) { 166 mPhoneDevice = new PhoneMediaDevice(mContext, mLocalBluetoothManager); 167 } 168 mMediaDevices.add(mPhoneDevice); 169 } 170 } 171 172 private void removePhoneMediaDeviceIfNecessary() { 173 // if PhoneMediaDevice is the last item in the list, remove it. 174 if (mMediaDevices.size() == 1 && mMediaDevices.contains(mPhoneDevice)) { 175 mMediaDevices.clear(); 176 } 177 } 178 179 void dispatchDeviceListUpdate() { 180 synchronized (mCallbacks) { 181 Collections.sort(mMediaDevices, COMPARATOR); 182 for (DeviceCallback callback : mCallbacks) { 183 callback.onDeviceListUpdate(new ArrayList<>(mMediaDevices)); 184 } 185 } 186 } 187 188 /** 189 * Stop scan MediaDevice 190 */ 191 public void stopScan() { 192 mBluetoothMediaManager.unregisterCallback(mMediaDeviceCallback); 193 mBluetoothMediaManager.stopScan(); 194 } 195 196 /** 197 * Find the MediaDevice through id. 198 * 199 * @param devices the list of MediaDevice 200 * @param id the unique id of MediaDevice 201 * @return MediaDevice 202 */ 203 public MediaDevice getMediaDeviceById(List<MediaDevice> devices, String id) { 204 for (MediaDevice mediaDevice : devices) { 205 if (mediaDevice.getId().equals(id)) { 206 return mediaDevice; 207 } 208 } 209 Log.i(TAG, "getMediaDeviceById() can't found device"); 210 return null; 211 } 212 213 /** 214 * Find the current connected MediaDevice. 215 * 216 * @return MediaDevice 217 */ 218 public MediaDevice getCurrentConnectedDevice() { 219 return mCurrentConnectedDevice; 220 } 221 222 private MediaDevice updateCurrentConnectedDevice() { 223 for (MediaDevice device : mMediaDevices) { 224 if (device instanceof BluetoothMediaDevice) { 225 if (isConnected(((BluetoothMediaDevice) device).getCachedDevice())) { 226 return device; 227 } 228 } 229 } 230 return mMediaDevices.contains(mPhoneDevice) ? mPhoneDevice : null; 231 } 232 233 private boolean isConnected(CachedBluetoothDevice device) { 234 return device.isActiveDevice(BluetoothProfile.A2DP) 235 || device.isActiveDevice(BluetoothProfile.HEARING_AID); 236 } 237 238 class MediaDeviceCallback implements MediaManager.MediaDeviceCallback { 239 @Override 240 public void onDeviceAdded(MediaDevice device) { 241 if (!mMediaDevices.contains(device)) { 242 mMediaDevices.add(device); 243 addPhoneDeviceIfNecessary(); 244 dispatchDeviceListUpdate(); 245 } 246 } 247 248 @Override 249 public void onDeviceListAdded(List<MediaDevice> devices) { 250 for (MediaDevice device : devices) { 251 if (getMediaDeviceById(mMediaDevices, device.getId()) == null) { 252 mMediaDevices.add(device); 253 } 254 } 255 addPhoneDeviceIfNecessary(); 256 mCurrentConnectedDevice = updateCurrentConnectedDevice(); 257 updatePhoneMediaDeviceSummary(); 258 dispatchDeviceListUpdate(); 259 } 260 261 private void updatePhoneMediaDeviceSummary() { 262 if (mPhoneDevice != null) { 263 ((PhoneMediaDevice) mPhoneDevice) 264 .updateSummary(mCurrentConnectedDevice == mPhoneDevice); 265 } 266 } 267 268 @Override 269 public void onDeviceRemoved(MediaDevice device) { 270 if (mMediaDevices.contains(device)) { 271 mMediaDevices.remove(device); 272 removePhoneMediaDeviceIfNecessary(); 273 dispatchDeviceListUpdate(); 274 } 275 } 276 277 @Override 278 public void onDeviceListRemoved(List<MediaDevice> devices) { 279 mMediaDevices.removeAll(devices); 280 removePhoneMediaDeviceIfNecessary(); 281 dispatchDeviceListUpdate(); 282 } 283 284 @Override 285 public void onConnectedDeviceChanged(String id) { 286 final MediaDevice connectDevice = getMediaDeviceById(mMediaDevices, id); 287 288 if (connectDevice == mCurrentConnectedDevice) { 289 Log.d(TAG, "onConnectedDeviceChanged() this device all ready connected!"); 290 return; 291 } 292 mCurrentConnectedDevice = connectDevice; 293 updatePhoneMediaDeviceSummary(); 294 dispatchDeviceListUpdate(); 295 } 296 297 @Override 298 public void onDeviceAttributesChanged() { 299 addPhoneDeviceIfNecessary(); 300 removePhoneMediaDeviceIfNecessary(); 301 dispatchDeviceListUpdate(); 302 } 303 } 304 305 306 /** 307 * Callback for notifying device information updating 308 */ 309 public interface DeviceCallback { 310 /** 311 * Callback for notifying device list updated. 312 * 313 * @param devices MediaDevice list 314 */ 315 void onDeviceListUpdate(List<MediaDevice> devices); 316 317 /** 318 * Callback for notifying the connected device is changed. 319 * 320 * @param device the changed connected MediaDevice 321 * @param state the current MediaDevice state, the possible values are: 322 * {@link MediaDeviceState#STATE_CONNECTED}, 323 * {@link MediaDeviceState#STATE_CONNECTING}, 324 * {@link MediaDeviceState#STATE_DISCONNECTED} 325 */ 326 void onSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state); 327 } 328 } 329