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