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