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