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