• 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_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