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 static android.media.MediaRoute2Info.TYPE_BLE_HEADSET; 19 import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP; 20 import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; 21 import static android.media.MediaRoute2Info.TYPE_DOCK; 22 import static android.media.MediaRoute2Info.TYPE_GROUP; 23 import static android.media.MediaRoute2Info.TYPE_HDMI; 24 import static android.media.MediaRoute2Info.TYPE_HEARING_AID; 25 import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; 26 import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; 27 import static android.media.MediaRoute2Info.TYPE_UNKNOWN; 28 import static android.media.MediaRoute2Info.TYPE_USB_ACCESSORY; 29 import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; 30 import static android.media.MediaRoute2Info.TYPE_USB_HEADSET; 31 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; 32 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; 33 34 import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED; 35 36 import android.annotation.SuppressLint; 37 import android.content.Context; 38 import android.graphics.drawable.Drawable; 39 import android.media.MediaRoute2Info; 40 import android.media.MediaRouter2Manager; 41 import android.media.NearbyDevice; 42 import android.text.TextUtils; 43 import android.util.Log; 44 45 import androidx.annotation.IntDef; 46 import androidx.annotation.VisibleForTesting; 47 48 import java.lang.annotation.Retention; 49 import java.lang.annotation.RetentionPolicy; 50 import java.util.ArrayList; 51 import java.util.List; 52 53 /** 54 * MediaDevice represents a media device(such like Bluetooth device, cast device and phone device). 55 */ 56 public abstract class MediaDevice implements Comparable<MediaDevice> { 57 private static final String TAG = "MediaDevice"; 58 59 @Retention(RetentionPolicy.SOURCE) 60 @IntDef({MediaDeviceType.TYPE_UNKNOWN, 61 MediaDeviceType.TYPE_PHONE_DEVICE, 62 MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE, 63 MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE, 64 MediaDeviceType.TYPE_FAST_PAIR_BLUETOOTH_DEVICE, 65 MediaDeviceType.TYPE_BLUETOOTH_DEVICE, 66 MediaDeviceType.TYPE_CAST_DEVICE, 67 MediaDeviceType.TYPE_CAST_GROUP_DEVICE}) 68 public @interface MediaDeviceType { 69 int TYPE_UNKNOWN = 0; 70 int TYPE_PHONE_DEVICE = 1; 71 int TYPE_USB_C_AUDIO_DEVICE = 2; 72 int TYPE_3POINT5_MM_AUDIO_DEVICE = 3; 73 int TYPE_FAST_PAIR_BLUETOOTH_DEVICE = 4; 74 int TYPE_BLUETOOTH_DEVICE = 5; 75 int TYPE_CAST_DEVICE = 6; 76 int TYPE_CAST_GROUP_DEVICE = 7; 77 } 78 79 @VisibleForTesting 80 int mType; 81 82 private int mConnectedRecord; 83 private int mState; 84 @NearbyDevice.RangeZone 85 private int mRangeZone = NearbyDevice.RANGE_UNKNOWN; 86 87 protected final Context mContext; 88 protected final MediaRoute2Info mRouteInfo; 89 protected final MediaRouter2Manager mRouterManager; 90 protected final String mPackageName; 91 MediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info, String packageName)92 MediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info, 93 String packageName) { 94 mContext = context; 95 mRouteInfo = info; 96 mRouterManager = routerManager; 97 mPackageName = packageName; 98 setType(info); 99 } 100 setType(MediaRoute2Info info)101 private void setType(MediaRoute2Info info) { 102 if (info == null) { 103 mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE; 104 return; 105 } 106 107 switch (info.getType()) { 108 case TYPE_GROUP: 109 mType = MediaDeviceType.TYPE_CAST_GROUP_DEVICE; 110 break; 111 case TYPE_BUILTIN_SPEAKER: 112 mType = MediaDeviceType.TYPE_PHONE_DEVICE; 113 break; 114 case TYPE_WIRED_HEADSET: 115 case TYPE_WIRED_HEADPHONES: 116 mType = MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE; 117 break; 118 case TYPE_USB_DEVICE: 119 case TYPE_USB_HEADSET: 120 case TYPE_USB_ACCESSORY: 121 case TYPE_DOCK: 122 case TYPE_HDMI: 123 mType = MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE; 124 break; 125 case TYPE_HEARING_AID: 126 case TYPE_BLUETOOTH_A2DP: 127 case TYPE_BLE_HEADSET: 128 mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE; 129 break; 130 case TYPE_UNKNOWN: 131 case TYPE_REMOTE_TV: 132 case TYPE_REMOTE_SPEAKER: 133 default: 134 mType = MediaDeviceType.TYPE_CAST_DEVICE; 135 break; 136 } 137 } 138 initDeviceRecord()139 void initDeviceRecord() { 140 ConnectionRecordManager.getInstance().fetchLastSelectedDevice(mContext); 141 mConnectedRecord = ConnectionRecordManager.getInstance().fetchConnectionRecord(mContext, 142 getId()); 143 } 144 getRangeZone()145 public @NearbyDevice.RangeZone int getRangeZone() { 146 return mRangeZone; 147 } 148 setRangeZone(@earbyDevice.RangeZone int rangeZone)149 public void setRangeZone(@NearbyDevice.RangeZone int rangeZone) { 150 mRangeZone = rangeZone; 151 } 152 153 /** 154 * Get name from MediaDevice. 155 * 156 * @return name of MediaDevice. 157 */ getName()158 public abstract String getName(); 159 160 /** 161 * Get summary from MediaDevice. 162 * 163 * @return summary of MediaDevice. 164 */ getSummary()165 public abstract String getSummary(); 166 167 /** 168 * Get icon of MediaDevice. 169 * 170 * @return drawable of icon. 171 */ getIcon()172 public abstract Drawable getIcon(); 173 174 /** 175 * Get icon of MediaDevice without background. 176 * 177 * @return drawable of icon 178 */ getIconWithoutBackground()179 public abstract Drawable getIconWithoutBackground(); 180 181 /** 182 * Get unique ID that represent MediaDevice 183 * @return unique id of MediaDevice 184 */ getId()185 public abstract String getId(); 186 187 /** 188 * Get disabled reason of device 189 * 190 * @return disabled reason of device 191 */ getDisableReason()192 public int getDisableReason() { 193 return -1; 194 } 195 196 /** 197 * Checks if device is has disabled reason 198 * 199 * @return true if device has disabled reason 200 */ hasDisabledReason()201 public boolean hasDisabledReason() { 202 return false; 203 } 204 205 /** 206 * Checks if device is suggested device from application 207 * 208 * @return true if device is suggested device 209 */ isSuggestedDevice()210 public boolean isSuggestedDevice() { 211 return false; 212 } 213 setConnectedRecord()214 void setConnectedRecord() { 215 mConnectedRecord++; 216 ConnectionRecordManager.getInstance().setConnectionRecord(mContext, getId(), 217 mConnectedRecord); 218 } 219 220 /** 221 * According the MediaDevice type to check whether we are connected to this MediaDevice. 222 * 223 * @return Whether it is connected. 224 */ isConnected()225 public abstract boolean isConnected(); 226 227 /** 228 * Request to set volume. 229 * 230 * @param volume is the new value. 231 */ 232 requestSetVolume(int volume)233 public void requestSetVolume(int volume) { 234 if (mRouteInfo == null) { 235 Log.w(TAG, "Unable to set volume. RouteInfo is empty"); 236 return; 237 } 238 mRouterManager.setRouteVolume(mRouteInfo, volume); 239 } 240 241 /** 242 * Get max volume from MediaDevice. 243 * 244 * @return max volume. 245 */ getMaxVolume()246 public int getMaxVolume() { 247 if (mRouteInfo == null) { 248 Log.w(TAG, "Unable to get max volume. RouteInfo is empty"); 249 return 0; 250 } 251 return mRouteInfo.getVolumeMax(); 252 } 253 254 /** 255 * Get current volume from MediaDevice. 256 * 257 * @return current volume. 258 */ getCurrentVolume()259 public int getCurrentVolume() { 260 if (mRouteInfo == null) { 261 Log.w(TAG, "Unable to get current volume. RouteInfo is empty"); 262 return 0; 263 } 264 return mRouteInfo.getVolume(); 265 } 266 267 /** 268 * Get application package name. 269 * 270 * @return package name. 271 */ getClientPackageName()272 public String getClientPackageName() { 273 if (mRouteInfo == null) { 274 Log.w(TAG, "Unable to get client package name. RouteInfo is empty"); 275 return null; 276 } 277 return mRouteInfo.getClientPackageName(); 278 } 279 280 /** 281 * Check if the device is Bluetooth LE Audio device. 282 * 283 * @return true if the RouteInfo equals TYPE_BLE_HEADSET. 284 */ isBLEDevice()285 public boolean isBLEDevice() { 286 return mRouteInfo.getType() == TYPE_BLE_HEADSET; 287 } 288 289 /** 290 * Get application label from MediaDevice. 291 * 292 * @return application label. 293 */ getDeviceType()294 public int getDeviceType() { 295 return mType; 296 } 297 298 /** 299 * Checks if route's volume is fixed, if true, we should disable volume control for the device. 300 * 301 * @return route for this device is fixed. 302 */ 303 @SuppressLint("NewApi") isVolumeFixed()304 public boolean isVolumeFixed() { 305 if (mRouteInfo == null) { 306 Log.w(TAG, "RouteInfo is empty, regarded as volume fixed."); 307 return true; 308 } 309 return mRouteInfo.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED; 310 } 311 312 /** 313 * Transfer MediaDevice for media 314 * 315 * @return result of transfer media 316 */ connect()317 public boolean connect() { 318 if (mRouteInfo == null) { 319 Log.w(TAG, "Unable to connect. RouteInfo is empty"); 320 return false; 321 } 322 setConnectedRecord(); 323 mRouterManager.selectRoute(mPackageName, mRouteInfo); 324 return true; 325 } 326 327 /** 328 * Stop transfer MediaDevice 329 */ disconnect()330 public void disconnect() { 331 } 332 333 /** 334 * Set current device's state 335 */ setState(@ocalMediaManager.MediaDeviceState int state)336 public void setState(@LocalMediaManager.MediaDeviceState int state) { 337 mState = state; 338 } 339 340 /** 341 * Get current device's state 342 * 343 * @return state of device 344 */ getState()345 public @LocalMediaManager.MediaDeviceState int getState() { 346 return mState; 347 } 348 349 /** 350 * Rules: 351 * 1. If there is one of the connected devices identified as a carkit or fast pair device, 352 * the fast pair device will be always on the first of the device list and carkit will be 353 * second. Rule 2 and Rule 3 can’t overrule this rule. 354 * 2. For devices without any usage data yet 355 * WiFi device group sorted by alphabetical order + BT device group sorted by alphabetical 356 * order + phone speaker 357 * 3. For devices with usage record. 358 * The most recent used one + device group with usage info sorted by how many times the 359 * device has been used. 360 * 4. The order is followed below rule: 361 * 1. Phone 362 * 2. USB-C audio device 363 * 3. 3.5 mm audio device 364 * 4. Bluetooth device 365 * 5. Cast device 366 * 6. Cast group device 367 * 368 * So the device list will look like 5 slots ranked as below. 369 * Rule 4 + Rule 1 + the most recently used device + Rule 3 + Rule 2 370 * Any slot could be empty. And available device will belong to one of the slots. 371 * 372 * @return a negative integer, zero, or a positive integer 373 * as this object is less than, equal to, or greater than the specified object. 374 */ 375 @Override compareTo(MediaDevice another)376 public int compareTo(MediaDevice another) { 377 if (another == null) { 378 return -1; 379 } 380 // Check Bluetooth device is have same connection state 381 if (isConnected() ^ another.isConnected()) { 382 if (isConnected()) { 383 return -1; 384 } else { 385 return 1; 386 } 387 } 388 389 if (getState() == STATE_SELECTED) { 390 return -1; 391 } else if (another.getState() == STATE_SELECTED) { 392 return 1; 393 } 394 395 if (mType == another.mType) { 396 // Check device is muting expected device 397 if (isMutingExpectedDevice()) { 398 return -1; 399 } else if (another.isMutingExpectedDevice()) { 400 return 1; 401 } 402 403 // Check fast pair device 404 if (isFastPairDevice()) { 405 return -1; 406 } else if (another.isFastPairDevice()) { 407 return 1; 408 } 409 410 // Check carkit 411 if (isCarKitDevice()) { 412 return -1; 413 } else if (another.isCarKitDevice()) { 414 return 1; 415 } 416 417 // Both devices have same connection status and type, compare the range zone 418 if (NearbyDevice.compareRangeZones(getRangeZone(), another.getRangeZone()) != 0) { 419 return NearbyDevice.compareRangeZones(getRangeZone(), another.getRangeZone()); 420 } 421 422 // Set last used device at the first item 423 final String lastSelectedDevice = ConnectionRecordManager.getInstance() 424 .getLastSelectedDevice(); 425 if (TextUtils.equals(lastSelectedDevice, getId())) { 426 return -1; 427 } else if (TextUtils.equals(lastSelectedDevice, another.getId())) { 428 return 1; 429 } 430 // Sort by how many times the device has been used if there is usage record 431 if ((mConnectedRecord != another.mConnectedRecord) 432 && (another.mConnectedRecord > 0 || mConnectedRecord > 0)) { 433 return (another.mConnectedRecord - mConnectedRecord); 434 } 435 436 // Both devices have never been used 437 // To devices with the same type, sort by alphabetical order 438 final String s1 = getName(); 439 final String s2 = another.getName(); 440 return s1.compareToIgnoreCase(s2); 441 } else { 442 // Both devices have never been used, the priority is: 443 // 1. Phone 444 // 2. USB-C audio device 445 // 3. 3.5 mm audio device 446 // 4. Bluetooth device 447 // 5. Cast device 448 // 6. Cast group device 449 return mType < another.mType ? -1 : 1; 450 } 451 } 452 453 /** 454 * Gets the supported features of the route. 455 */ getFeatures()456 public List<String> getFeatures() { 457 if (mRouteInfo == null) { 458 Log.w(TAG, "Unable to get features. RouteInfo is empty"); 459 return new ArrayList<>(); 460 } 461 return mRouteInfo.getFeatures(); 462 } 463 464 /** 465 * Check if it is CarKit device 466 * @return true if it is CarKit device 467 */ isCarKitDevice()468 protected boolean isCarKitDevice() { 469 return false; 470 } 471 472 /** 473 * Check if it is FastPair device 474 * @return {@code true} if it is FastPair device, otherwise return {@code false} 475 */ isFastPairDevice()476 protected boolean isFastPairDevice() { 477 return false; 478 } 479 480 /** 481 * Check if it is muting expected device 482 * @return {@code true} if it is muting expected device, otherwise return {@code false} 483 */ isMutingExpectedDevice()484 public boolean isMutingExpectedDevice() { 485 return false; 486 } 487 488 @Override equals(Object obj)489 public boolean equals(Object obj) { 490 if (!(obj instanceof MediaDevice)) { 491 return false; 492 } 493 final MediaDevice otherDevice = (MediaDevice) obj; 494 return otherDevice.getId().equals(getId()); 495 } 496 } 497