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