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_AUX_LINE; 19 import static android.media.MediaRoute2Info.TYPE_BLE_HEADSET; 20 import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP; 21 import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; 22 import static android.media.MediaRoute2Info.TYPE_DOCK; 23 import static android.media.MediaRoute2Info.TYPE_GROUP; 24 import static android.media.MediaRoute2Info.TYPE_HDMI; 25 import static android.media.MediaRoute2Info.TYPE_HDMI_ARC; 26 import static android.media.MediaRoute2Info.TYPE_HDMI_EARC; 27 import static android.media.MediaRoute2Info.TYPE_HEARING_AID; 28 import static android.media.MediaRoute2Info.TYPE_LINE_ANALOG; 29 import static android.media.MediaRoute2Info.TYPE_LINE_DIGITAL; 30 import static android.media.MediaRoute2Info.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER; 31 import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; 32 import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; 33 import static android.media.MediaRoute2Info.TYPE_UNKNOWN; 34 import static android.media.MediaRoute2Info.TYPE_USB_ACCESSORY; 35 import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; 36 import static android.media.MediaRoute2Info.TYPE_USB_HEADSET; 37 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; 38 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; 39 import static android.media.RouteListingPreference.Item.FLAG_ONGOING_SESSION; 40 import static android.media.RouteListingPreference.Item.FLAG_ONGOING_SESSION_MANAGED; 41 import static android.media.RouteListingPreference.Item.FLAG_SUGGESTED; 42 import static android.media.RouteListingPreference.Item.SUBTEXT_AD_ROUTING_DISALLOWED; 43 import static android.media.RouteListingPreference.Item.SUBTEXT_CUSTOM; 44 import static android.media.RouteListingPreference.Item.SUBTEXT_DEVICE_LOW_POWER; 45 import static android.media.RouteListingPreference.Item.SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED; 46 import static android.media.RouteListingPreference.Item.SUBTEXT_ERROR_UNKNOWN; 47 import static android.media.RouteListingPreference.Item.SUBTEXT_NONE; 48 import static android.media.RouteListingPreference.Item.SUBTEXT_SUBSCRIPTION_REQUIRED; 49 import static android.media.RouteListingPreference.Item.SUBTEXT_TRACK_UNSUPPORTED; 50 import static android.media.RouteListingPreference.Item.SUBTEXT_UNAUTHORIZED; 51 52 import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED; 53 import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER; 54 55 import android.annotation.NonNull; 56 import android.annotation.Nullable; 57 import android.annotation.SuppressLint; 58 import android.content.Context; 59 import android.graphics.drawable.Drawable; 60 import android.media.MediaRoute2Info; 61 import android.media.NearbyDevice; 62 import android.media.RouteListingPreference; 63 import android.os.Build; 64 import android.text.TextUtils; 65 import android.util.Log; 66 67 import androidx.annotation.DoNotInline; 68 import androidx.annotation.IntDef; 69 import androidx.annotation.RequiresApi; 70 import androidx.annotation.VisibleForTesting; 71 72 import com.android.settingslib.R; 73 74 import java.lang.annotation.Retention; 75 import java.lang.annotation.RetentionPolicy; 76 import java.util.ArrayList; 77 import java.util.List; 78 79 /** 80 * MediaDevice represents a media device(such like Bluetooth device, cast device and phone device). 81 */ 82 public abstract class MediaDevice implements Comparable<MediaDevice> { 83 private static final String TAG = "MediaDevice"; 84 85 @Retention(RetentionPolicy.SOURCE) 86 @IntDef({MediaDeviceType.TYPE_UNKNOWN, 87 MediaDeviceType.TYPE_PHONE_DEVICE, 88 MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE, 89 MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE, 90 MediaDeviceType.TYPE_FAST_PAIR_BLUETOOTH_DEVICE, 91 MediaDeviceType.TYPE_BLUETOOTH_DEVICE, 92 MediaDeviceType.TYPE_CAST_DEVICE, 93 MediaDeviceType.TYPE_CAST_GROUP_DEVICE, 94 MediaDeviceType.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER}) 95 public @interface MediaDeviceType { 96 int TYPE_UNKNOWN = 0; 97 int TYPE_PHONE_DEVICE = 1; 98 int TYPE_USB_C_AUDIO_DEVICE = 2; 99 int TYPE_3POINT5_MM_AUDIO_DEVICE = 3; 100 int TYPE_FAST_PAIR_BLUETOOTH_DEVICE = 4; 101 int TYPE_BLUETOOTH_DEVICE = 5; 102 int TYPE_CAST_DEVICE = 6; 103 int TYPE_CAST_GROUP_DEVICE = 7; 104 int TYPE_REMOTE_AUDIO_VIDEO_RECEIVER = 8; 105 } 106 107 @Retention(RetentionPolicy.SOURCE) 108 @IntDef({SelectionBehavior.SELECTION_BEHAVIOR_NONE, 109 SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER, 110 SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP 111 }) 112 public @interface SelectionBehavior { 113 int SELECTION_BEHAVIOR_NONE = 0; 114 int SELECTION_BEHAVIOR_TRANSFER = 1; 115 int SELECTION_BEHAVIOR_GO_TO_APP = 2; 116 } 117 118 @VisibleForTesting 119 int mType; 120 121 private int mConnectedRecord; 122 private int mState; 123 @NearbyDevice.RangeZone 124 private int mRangeZone = NearbyDevice.RANGE_UNKNOWN; 125 126 protected final Context mContext; 127 protected final MediaRoute2Info mRouteInfo; 128 protected final RouteListingPreference.Item mItem; 129 MediaDevice( @onNull Context context, @Nullable MediaRoute2Info info, @Nullable RouteListingPreference.Item item)130 MediaDevice( 131 @NonNull Context context, 132 @Nullable MediaRoute2Info info, 133 @Nullable RouteListingPreference.Item item) { 134 mContext = context; 135 mRouteInfo = info; 136 mItem = item; 137 setType(info); 138 } 139 140 // MediaRoute2Info.getType was made public on API 34, but exists since API 30. 141 @SuppressWarnings("NewApi") setType(MediaRoute2Info info)142 private void setType(MediaRoute2Info info) { 143 if (info == null) { 144 mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE; 145 return; 146 } 147 switch (info.getType()) { 148 case TYPE_GROUP: 149 mType = MediaDeviceType.TYPE_CAST_GROUP_DEVICE; 150 break; 151 case TYPE_BUILTIN_SPEAKER: 152 mType = MediaDeviceType.TYPE_PHONE_DEVICE; 153 break; 154 case TYPE_WIRED_HEADSET: 155 case TYPE_WIRED_HEADPHONES: 156 case TYPE_LINE_DIGITAL: 157 case TYPE_LINE_ANALOG: 158 case TYPE_AUX_LINE: 159 mType = MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE; 160 break; 161 case TYPE_USB_DEVICE: 162 case TYPE_USB_HEADSET: 163 case TYPE_USB_ACCESSORY: 164 case TYPE_DOCK: 165 case TYPE_HDMI: 166 case TYPE_HDMI_ARC: 167 case TYPE_HDMI_EARC: 168 mType = MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE; 169 break; 170 case TYPE_HEARING_AID: 171 case TYPE_BLUETOOTH_A2DP: 172 case TYPE_BLE_HEADSET: 173 mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE; 174 break; 175 case TYPE_REMOTE_AUDIO_VIDEO_RECEIVER: 176 mType = MediaDeviceType.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER; 177 break; 178 case TYPE_UNKNOWN: 179 case TYPE_REMOTE_TV: 180 case TYPE_REMOTE_SPEAKER: 181 default: 182 mType = MediaDeviceType.TYPE_CAST_DEVICE; 183 break; 184 } 185 } 186 initDeviceRecord()187 void initDeviceRecord() { 188 ConnectionRecordManager.getInstance().fetchLastSelectedDevice(mContext); 189 mConnectedRecord = ConnectionRecordManager.getInstance().fetchConnectionRecord(mContext, 190 getId()); 191 } 192 getRangeZone()193 public @NearbyDevice.RangeZone int getRangeZone() { 194 return mRangeZone; 195 } 196 setRangeZone(@earbyDevice.RangeZone int rangeZone)197 public void setRangeZone(@NearbyDevice.RangeZone int rangeZone) { 198 mRangeZone = rangeZone; 199 } 200 201 /** 202 * Get name from MediaDevice. 203 * 204 * @return name of MediaDevice. 205 */ getName()206 public abstract String getName(); 207 208 /** 209 * Get summary from MediaDevice. 210 * 211 * @return summary of MediaDevice. 212 */ getSummary()213 public abstract String getSummary(); 214 215 /** 216 * Get summary from MediaDevice for TV with low batter states in a different color if 217 * applicable. 218 * 219 * @param lowBatteryColorRes Color resource for the part of the CharSequence that describes a 220 * low battery state. 221 */ getSummaryForTv(int lowBatteryColorRes)222 public CharSequence getSummaryForTv(int lowBatteryColorRes) { 223 return getSummary(); 224 } 225 226 /** 227 * Get icon of MediaDevice. 228 * 229 * @return drawable of icon. 230 */ getIcon()231 public abstract Drawable getIcon(); 232 233 /** 234 * Get icon of MediaDevice without background. 235 * 236 * @return drawable of icon 237 */ getIconWithoutBackground()238 public abstract Drawable getIconWithoutBackground(); 239 240 /** 241 * Get unique ID that represent MediaDevice 242 * 243 * @return unique id of MediaDevice 244 */ getId()245 public abstract String getId(); 246 247 /** Returns {@code true} if the device has a non-null {@link RouteListingPreference.Item}. */ hasRouteListingPreferenceItem()248 public boolean hasRouteListingPreferenceItem() { 249 return mItem != null; 250 } 251 252 /** 253 * Get selection behavior of device 254 * 255 * @return selection behavior of device 256 */ 257 @SelectionBehavior getSelectionBehavior()258 public int getSelectionBehavior() { 259 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && mItem != null 260 ? mItem.getSelectionBehavior() : SELECTION_BEHAVIOR_TRANSFER; 261 } 262 263 /** 264 * Checks if device is has subtext 265 * 266 * @return true if device has subtext 267 */ hasSubtext()268 public boolean hasSubtext() { 269 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE 270 && mItem != null 271 && mItem.getSubText() != SUBTEXT_NONE; 272 } 273 274 /** 275 * Get subtext of device 276 * 277 * @return subtext of device 278 */ 279 @RouteListingPreference.Item.SubText getSubtext()280 public int getSubtext() { 281 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && mItem != null 282 ? mItem.getSubText() : SUBTEXT_NONE; 283 } 284 285 /** 286 * Returns subtext string for current route. 287 * 288 * @return subtext string for this route 289 */ getSubtextString()290 public String getSubtextString() { 291 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && mItem != null 292 ? Api34Impl.composeSubtext(mItem, mContext) : null; 293 } 294 295 /** 296 * Checks if device has ongoing shared session, which allow user to join 297 * 298 * @return true if device has ongoing session 299 */ hasOngoingSession()300 public boolean hasOngoingSession() { 301 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE 302 && Api34Impl.hasOngoingSession(mItem); 303 } 304 305 /** 306 * Checks if device is the host for ongoing shared session, which allow user to adjust volume 307 * 308 * @return true if device is the host for ongoing shared session 309 */ isHostForOngoingSession()310 public boolean isHostForOngoingSession() { 311 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE 312 && Api34Impl.isHostForOngoingSession(mItem); 313 } 314 315 /** 316 * Checks if device is suggested device from application 317 * 318 * @return true if device is suggested device 319 */ isSuggestedDevice()320 public boolean isSuggestedDevice() { 321 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE 322 && Api34Impl.isSuggestedDevice(mItem); 323 } 324 setConnectedRecord()325 void setConnectedRecord() { 326 mConnectedRecord++; 327 ConnectionRecordManager.getInstance().setConnectionRecord(mContext, getId(), 328 mConnectedRecord); 329 } 330 331 /** 332 * According the MediaDevice type to check whether we are connected to this MediaDevice. 333 * 334 * @return Whether it is connected. 335 */ isConnected()336 public abstract boolean isConnected(); 337 338 /** 339 * Get max volume from MediaDevice. 340 * 341 * @return max volume. 342 */ getMaxVolume()343 public int getMaxVolume() { 344 if (mRouteInfo == null) { 345 Log.w(TAG, "Unable to get max volume. RouteInfo is empty"); 346 return 0; 347 } 348 return mRouteInfo.getVolumeMax(); 349 } 350 351 /** 352 * Get current volume from MediaDevice. 353 * 354 * @return current volume. 355 */ getCurrentVolume()356 public int getCurrentVolume() { 357 if (mRouteInfo == null) { 358 Log.w(TAG, "Unable to get current volume. RouteInfo is empty"); 359 return 0; 360 } 361 return mRouteInfo.getVolume(); 362 } 363 364 /** 365 * Get application package name. 366 * 367 * @return package name. 368 */ getClientPackageName()369 public String getClientPackageName() { 370 if (mRouteInfo == null) { 371 Log.w(TAG, "Unable to get client package name. RouteInfo is empty"); 372 return null; 373 } 374 return mRouteInfo.getClientPackageName(); 375 } 376 377 /** 378 * Check if the device is Bluetooth LE Audio device. 379 * 380 * @return true if the RouteInfo equals TYPE_BLE_HEADSET. 381 */ 382 // MediaRoute2Info.getType was made public on API 34, but exists since API 30. 383 @SuppressWarnings("NewApi") isBLEDevice()384 public boolean isBLEDevice() { 385 return mRouteInfo.getType() == TYPE_BLE_HEADSET; 386 } 387 388 /** 389 * Get application label from MediaDevice. 390 * 391 * @return application label. 392 */ getDeviceType()393 public int getDeviceType() { 394 return mType; 395 } 396 397 /** 398 * Get the {@link MediaRoute2Info.Type} of the device. 399 */ getRouteType()400 public int getRouteType() { 401 if (mRouteInfo == null) { 402 return TYPE_UNKNOWN; 403 } 404 return mRouteInfo.getType(); 405 } 406 407 /** 408 * Checks if route's volume is fixed, if true, we should disable volume control for the device. 409 * 410 * @return route for this device is fixed. 411 */ 412 @SuppressLint("NewApi") isVolumeFixed()413 public boolean isVolumeFixed() { 414 if (mRouteInfo == null) { 415 Log.w(TAG, "RouteInfo is empty, regarded as volume fixed."); 416 return true; 417 } 418 return mRouteInfo.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED; 419 } 420 421 /** 422 * Set current device's state 423 */ setState(@ocalMediaManager.MediaDeviceState int state)424 public void setState(@LocalMediaManager.MediaDeviceState int state) { 425 mState = state; 426 } 427 428 /** 429 * Get current device's state 430 * 431 * @return state of device 432 */ getState()433 public @LocalMediaManager.MediaDeviceState int getState() { 434 return mState; 435 } 436 437 /** 438 * Rules: 439 * 1. If there is one of the connected devices identified as a carkit or fast pair device, 440 * the fast pair device will be always on the first of the device list and carkit will be 441 * second. Rule 2 and Rule 3 can’t overrule this rule. 442 * 2. For devices without any usage data yet 443 * WiFi device group sorted by alphabetical order + BT device group sorted by alphabetical 444 * order + phone speaker 445 * 3. For devices with usage record. 446 * The most recent used one + device group with usage info sorted by how many times the 447 * device has been used. 448 * 4. The order is followed below rule: 449 * 1. Phone 450 * 2. USB-C audio device 451 * 3. 3.5 mm audio device 452 * 4. Bluetooth device 453 * 5. Cast device 454 * 6. Cast group device 455 * 456 * So the device list will look like 5 slots ranked as below. 457 * Rule 4 + Rule 1 + the most recently used device + Rule 3 + Rule 2 458 * Any slot could be empty. And available device will belong to one of the slots. 459 * 460 * @return a negative integer, zero, or a positive integer 461 * as this object is less than, equal to, or greater than the specified object. 462 */ 463 @Override compareTo(MediaDevice another)464 public int compareTo(MediaDevice another) { 465 if (another == null) { 466 return -1; 467 } 468 // Check Bluetooth device is have same connection state 469 if (isConnected() ^ another.isConnected()) { 470 if (isConnected()) { 471 return -1; 472 } else { 473 return 1; 474 } 475 } 476 477 if (getState() == STATE_SELECTED) { 478 return -1; 479 } else if (another.getState() == STATE_SELECTED) { 480 return 1; 481 } 482 483 if (mType == another.mType) { 484 // Check device is muting expected device 485 if (isMutingExpectedDevice()) { 486 return -1; 487 } else if (another.isMutingExpectedDevice()) { 488 return 1; 489 } 490 491 // Check fast pair device 492 if (isFastPairDevice()) { 493 return -1; 494 } else if (another.isFastPairDevice()) { 495 return 1; 496 } 497 498 // Check carkit 499 if (isCarKitDevice()) { 500 return -1; 501 } else if (another.isCarKitDevice()) { 502 return 1; 503 } 504 505 // Both devices have same connection status and type, compare the range zone 506 if (NearbyDevice.compareRangeZones(getRangeZone(), another.getRangeZone()) != 0) { 507 return NearbyDevice.compareRangeZones(getRangeZone(), another.getRangeZone()); 508 } 509 510 // Set last used device at the first item 511 final String lastSelectedDevice = ConnectionRecordManager.getInstance() 512 .getLastSelectedDevice(); 513 if (TextUtils.equals(lastSelectedDevice, getId())) { 514 return -1; 515 } else if (TextUtils.equals(lastSelectedDevice, another.getId())) { 516 return 1; 517 } 518 // Sort by how many times the device has been used if there is usage record 519 if ((mConnectedRecord != another.mConnectedRecord) 520 && (another.mConnectedRecord > 0 || mConnectedRecord > 0)) { 521 return (another.mConnectedRecord - mConnectedRecord); 522 } 523 524 // Both devices have never been used 525 // To devices with the same type, sort by alphabetical order 526 final String s1 = getName(); 527 final String s2 = another.getName(); 528 return s1.compareToIgnoreCase(s2); 529 } else { 530 // Both devices have never been used, the priority is: 531 // 1. Phone 532 // 2. USB-C audio device 533 // 3. 3.5 mm audio device 534 // 4. Bluetooth device 535 // 5. Cast device 536 // 6. Cast group device 537 return mType < another.mType ? -1 : 1; 538 } 539 } 540 541 /** 542 * Gets the supported features of the route. 543 */ getFeatures()544 public List<String> getFeatures() { 545 if (mRouteInfo == null) { 546 Log.w(TAG, "Unable to get features. RouteInfo is empty"); 547 return new ArrayList<>(); 548 } 549 return mRouteInfo.getFeatures(); 550 } 551 552 /** 553 * Check if it is CarKit device 554 * @return true if it is CarKit device 555 */ isCarKitDevice()556 protected boolean isCarKitDevice() { 557 return false; 558 } 559 560 /** 561 * Check if it is FastPair device 562 * @return {@code true} if it is FastPair device, otherwise return {@code false} 563 */ isFastPairDevice()564 protected boolean isFastPairDevice() { 565 return false; 566 } 567 568 /** 569 * Check if it is muting expected device 570 * @return {@code true} if it is muting expected device, otherwise return {@code false} 571 */ isMutingExpectedDevice()572 public boolean isMutingExpectedDevice() { 573 return false; 574 } 575 576 @Override equals(Object obj)577 public boolean equals(Object obj) { 578 if (!(obj instanceof MediaDevice)) { 579 return false; 580 } 581 final MediaDevice otherDevice = (MediaDevice) obj; 582 return otherDevice.getId().equals(getId()); 583 } 584 585 @RequiresApi(34) 586 private static class Api34Impl { 587 @DoNotInline isHostForOngoingSession(RouteListingPreference.Item item)588 static boolean isHostForOngoingSession(RouteListingPreference.Item item) { 589 int flags = item != null ? item.getFlags() : 0; 590 return (flags & FLAG_ONGOING_SESSION) != 0 591 && (flags & FLAG_ONGOING_SESSION_MANAGED) != 0; 592 } 593 594 @DoNotInline isSuggestedDevice(RouteListingPreference.Item item)595 static boolean isSuggestedDevice(RouteListingPreference.Item item) { 596 return item != null && (item.getFlags() & FLAG_SUGGESTED) != 0; 597 } 598 599 @DoNotInline hasOngoingSession(RouteListingPreference.Item item)600 static boolean hasOngoingSession(RouteListingPreference.Item item) { 601 return item != null && (item.getFlags() & FLAG_ONGOING_SESSION) != 0; 602 } 603 604 @DoNotInline composeSubtext(RouteListingPreference.Item item, Context context)605 static String composeSubtext(RouteListingPreference.Item item, Context context) { 606 switch (item.getSubText()) { 607 case SUBTEXT_ERROR_UNKNOWN: 608 return context.getString(R.string.media_output_status_unknown_error); 609 case SUBTEXT_SUBSCRIPTION_REQUIRED: 610 return context.getString(R.string.media_output_status_require_premium); 611 case SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED: 612 return context.getString(R.string.media_output_status_not_support_downloads); 613 case SUBTEXT_AD_ROUTING_DISALLOWED: 614 return context.getString(R.string.media_output_status_try_after_ad); 615 case SUBTEXT_DEVICE_LOW_POWER: 616 return context.getString(R.string.media_output_status_device_in_low_power_mode); 617 case SUBTEXT_UNAUTHORIZED: 618 return context.getString(R.string.media_output_status_unauthorized); 619 case SUBTEXT_TRACK_UNSUPPORTED: 620 return context.getString(R.string.media_output_status_track_unsupported); 621 case SUBTEXT_CUSTOM: 622 return (String) item.getCustomSubtextMessage(); 623 } 624 return ""; 625 } 626 } 627 } 628