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.content.pm.PackageManager.FEATURE_PC; 19 import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; 20 import static android.media.MediaRoute2Info.TYPE_DOCK; 21 import static android.media.MediaRoute2Info.TYPE_HDMI; 22 import static android.media.MediaRoute2Info.TYPE_HDMI_ARC; 23 import static android.media.MediaRoute2Info.TYPE_HDMI_EARC; 24 import static android.media.MediaRoute2Info.TYPE_USB_ACCESSORY; 25 import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; 26 import static android.media.MediaRoute2Info.TYPE_USB_HEADSET; 27 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; 28 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; 29 import static android.media.MediaRoute2Info.TYPE_LINE_DIGITAL; 30 import static android.media.MediaRoute2Info.TYPE_LINE_ANALOG; 31 import static android.media.MediaRoute2Info.TYPE_AUX_LINE; 32 33 import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER; 34 35 import android.Manifest; 36 import android.content.Context; 37 import android.content.pm.PackageManager; 38 import android.graphics.drawable.Drawable; 39 import android.hardware.hdmi.HdmiControlManager; 40 import android.hardware.hdmi.HdmiDeviceInfo; 41 import android.hardware.hdmi.HdmiPortInfo; 42 import android.media.MediaRoute2Info; 43 import android.media.RouteListingPreference; 44 import android.os.Build; 45 import android.os.SystemProperties; 46 import android.util.Log; 47 48 import androidx.annotation.NonNull; 49 import androidx.annotation.Nullable; 50 import androidx.annotation.VisibleForTesting; 51 52 import com.android.settingslib.R; 53 import com.android.settingslib.media.flags.Flags; 54 55 import java.util.Arrays; 56 import java.util.List; 57 58 /** 59 * PhoneMediaDevice extends MediaDevice to represents Phone device. 60 */ 61 public class PhoneMediaDevice extends MediaDevice { 62 63 private static final String TAG = "PhoneMediaDevice"; 64 65 public static final String PHONE_ID = "phone_media_device_id"; 66 // For 3.5 mm wired headset 67 public static final String WIRED_HEADSET_ID = "wired_headset_media_device_id"; 68 public static final String USB_HEADSET_ID = "usb_headset_media_device_id"; 69 70 private String mSummary = ""; 71 72 private final DeviceIconUtil mDeviceIconUtil; 73 74 /** Returns this device name for media transfer. */ getMediaTransferThisDeviceName(@onNull Context context)75 public static @NonNull String getMediaTransferThisDeviceName(@NonNull Context context) { 76 if (isTv(context)) { 77 return Build.MODEL; 78 } else if (isTablet()) { 79 return context.getString(R.string.media_transfer_this_device_name_tablet); 80 } else if (inputRoutingEnabledAndIsDesktop(context)) { 81 return context.getString(R.string.media_transfer_this_device_name_desktop); 82 } else { 83 return context.getString(R.string.media_transfer_this_device_name); 84 } 85 } 86 87 /** Returns the device name for the given {@code routeInfo}. */ getSystemRouteNameFromType( @onNull Context context, @NonNull MediaRoute2Info routeInfo)88 public static String getSystemRouteNameFromType( 89 @NonNull Context context, @NonNull MediaRoute2Info routeInfo) { 90 CharSequence name; 91 boolean isTv = isTv(context); 92 switch (routeInfo.getType()) { 93 case TYPE_WIRED_HEADSET: 94 case TYPE_WIRED_HEADPHONES: 95 name = 96 inputRoutingEnabledAndIsDesktop(context) 97 ? context.getString(R.string.media_transfer_headphone_name) 98 : context.getString(R.string.media_transfer_wired_headphone_name); 99 break; 100 case TYPE_USB_DEVICE: 101 case TYPE_USB_HEADSET: 102 case TYPE_USB_ACCESSORY: 103 name = 104 inputRoutingEnabledAndIsDesktop(context) 105 ? routeInfo.getName() 106 : context.getString(R.string.media_transfer_wired_headphone_name); 107 break; 108 case TYPE_DOCK: 109 name = context.getString(R.string.media_transfer_dock_speaker_device_name); 110 break; 111 case TYPE_BUILTIN_SPEAKER: 112 name = getMediaTransferThisDeviceName(context); 113 break; 114 case TYPE_HDMI: 115 name = context.getString(isTv ? R.string.tv_media_transfer_hdmi_title : 116 R.string.media_transfer_external_device_name); 117 break; 118 case TYPE_HDMI_ARC: 119 case TYPE_HDMI_EARC: 120 if (isTv) { 121 String deviceName = getHdmiOutDeviceName(context); 122 if (deviceName != null) { 123 name = deviceName; 124 } else { 125 name = context.getString(R.string.tv_media_transfer_arc_fallback_title); 126 } 127 } else { 128 name = context.getString(R.string.media_transfer_external_device_name); 129 } 130 break; 131 case TYPE_LINE_DIGITAL: 132 name = context.getString(R.string.media_transfer_digital_line_name); 133 break; 134 case TYPE_LINE_ANALOG: 135 name = context.getString(R.string.media_transfer_analog_line_name); 136 break; 137 case TYPE_AUX_LINE: 138 name = context.getString(R.string.media_transfer_aux_line_name); 139 break; 140 default: 141 name = context.getString(R.string.media_transfer_default_device_name); 142 break; 143 } 144 return name.toString(); 145 } 146 PhoneMediaDevice( @onNull Context context, @NonNull MediaRoute2Info info, @Nullable RouteListingPreference.Item item)147 PhoneMediaDevice( 148 @NonNull Context context, 149 @NonNull MediaRoute2Info info, 150 @Nullable RouteListingPreference.Item item) { 151 super(context, info, item); 152 mDeviceIconUtil = new DeviceIconUtil(mContext); 153 initDeviceRecord(); 154 } 155 isTv(Context context)156 static boolean isTv(Context context) { 157 return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) 158 && Flags.enableTvMediaOutputDialog(); 159 } 160 isTablet()161 static boolean isTablet() { 162 return Arrays.asList(SystemProperties.get("ro.build.characteristics").split(",")) 163 .contains("tablet"); 164 } 165 isDesktop(@onNull Context context)166 public static boolean isDesktop(@NonNull Context context) { 167 return context.getPackageManager().hasSystemFeature(FEATURE_PC); 168 } 169 inputRoutingEnabledAndIsDesktop(@onNull Context context)170 public static boolean inputRoutingEnabledAndIsDesktop(@NonNull Context context) { 171 return com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl() 172 && isDesktop(context); 173 } 174 175 // MediaRoute2Info.getType was made public on API 34, but exists since API 30. 176 @SuppressWarnings("NewApi") 177 @Override getName()178 public String getName() { 179 return getSystemRouteNameFromType(mContext, mRouteInfo); 180 } 181 182 @Override getSelectionBehavior()183 public int getSelectionBehavior() { 184 // We don't allow apps to override the selection behavior of system routes. 185 return SELECTION_BEHAVIOR_TRANSFER; 186 } 187 getHdmiOutDeviceName(Context context)188 private static String getHdmiOutDeviceName(Context context) { 189 HdmiControlManager hdmiControlManager; 190 if (context.checkCallingOrSelfPermission(Manifest.permission.HDMI_CEC) 191 == PackageManager.PERMISSION_GRANTED) { 192 hdmiControlManager = context.getSystemService(HdmiControlManager.class); 193 } else { 194 Log.w(TAG, "Could not get HDMI device name, android.permission.HDMI_CEC denied"); 195 return null; 196 } 197 198 HdmiPortInfo hdmiOutputPortInfo = null; 199 for (HdmiPortInfo hdmiPortInfo : hdmiControlManager.getPortInfo()) { 200 if (hdmiPortInfo.getType() == HdmiPortInfo.PORT_OUTPUT) { 201 hdmiOutputPortInfo = hdmiPortInfo; 202 break; 203 } 204 } 205 if (hdmiOutputPortInfo == null) { 206 return null; 207 } 208 List<HdmiDeviceInfo> connectedDevices = hdmiControlManager.getConnectedDevices(); 209 for (HdmiDeviceInfo deviceInfo : connectedDevices) { 210 if (deviceInfo.getPortId() == hdmiOutputPortInfo.getId()) { 211 String deviceName = deviceInfo.getDisplayName(); 212 if (deviceName != null && !deviceName.isEmpty()) { 213 return deviceName; 214 } 215 } 216 } 217 return null; 218 } 219 220 @Override getSummary()221 public String getSummary() { 222 if (!isTv(mContext)) { 223 return mSummary; 224 } 225 switch (mRouteInfo.getType()) { 226 case TYPE_BUILTIN_SPEAKER: 227 return mContext.getString(R.string.tv_media_transfer_internal_speakers); 228 case TYPE_HDMI_ARC: 229 if (getHdmiOutDeviceName(mContext) == null) { 230 // Connection type is already part of the title. 231 return mContext.getString(R.string.tv_media_transfer_connected); 232 } 233 return mContext.getString(R.string.tv_media_transfer_arc_subtitle); 234 case TYPE_HDMI_EARC: 235 if (getHdmiOutDeviceName(mContext) == null) { 236 // Connection type is already part of the title. 237 return mContext.getString(R.string.tv_media_transfer_connected); 238 } 239 return mContext.getString(R.string.tv_media_transfer_earc_subtitle); 240 default: 241 return null; 242 } 243 244 } 245 246 @Override getIcon()247 public Drawable getIcon() { 248 return getIconWithoutBackground(); 249 } 250 251 @Override getIconWithoutBackground()252 public Drawable getIconWithoutBackground() { 253 return mContext.getDrawable(getDrawableResId()); 254 } 255 256 // MediaRoute2Info.getType was made public on API 34, but exists since API 30. 257 @SuppressWarnings("NewApi") 258 @VisibleForTesting getDrawableResId()259 int getDrawableResId() { 260 return mDeviceIconUtil.getIconResIdFromMediaRouteType(mRouteInfo.getType()); 261 } 262 263 // MediaRoute2Info.getType was made public on API 34, but exists since API 30. 264 @SuppressWarnings("NewApi") 265 @Override getId()266 public String getId() { 267 if (com.android.media.flags.Flags.enableAudioPoliciesDeviceAndBluetoothController()) { 268 // Note: be careful when removing this flag. Instead of just removing it, you might want 269 // to replace it with SDK_INT >= 35. Explanation: The presence of SDK checks in settings 270 // lib suggests that a mainline component may depend on this code. Which means removing 271 // this "if" (and using always the route info id) could mean a regression on mainline 272 // code running on a device that's running API 34 or older. Unfortunately, we cannot 273 // check the API level at the moment of writing this code because the API level has not 274 // been bumped, yet. 275 return mRouteInfo.getId(); 276 } 277 278 String id; 279 switch (mRouteInfo.getType()) { 280 case TYPE_WIRED_HEADSET: 281 case TYPE_WIRED_HEADPHONES: 282 case TYPE_LINE_ANALOG: 283 case TYPE_LINE_DIGITAL: 284 case TYPE_AUX_LINE: 285 id = WIRED_HEADSET_ID; 286 break; 287 case TYPE_USB_DEVICE: 288 case TYPE_USB_HEADSET: 289 case TYPE_USB_ACCESSORY: 290 case TYPE_DOCK: 291 case TYPE_HDMI: 292 case TYPE_HDMI_ARC: 293 case TYPE_HDMI_EARC: 294 id = USB_HEADSET_ID; 295 break; 296 case TYPE_BUILTIN_SPEAKER: 297 default: 298 id = PHONE_ID; 299 break; 300 } 301 return id; 302 } 303 304 @Override isConnected()305 public boolean isConnected() { 306 return true; 307 } 308 309 /** 310 * According current active device is {@link PhoneMediaDevice} or not to update summary. 311 */ updateSummary(boolean isActive)312 public void updateSummary(boolean isActive) { 313 mSummary = isActive 314 ? mContext.getString(R.string.bluetooth_active_no_battery_level) 315 : ""; 316 } 317 } 318