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