• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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