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