1 /*
2  * Copyright (C) 2014 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 
17 package com.android.tv.settings.accessories;
18 
19 import android.bluetooth.BluetoothAdapter;
20 import android.bluetooth.BluetoothClass;
21 import android.bluetooth.BluetoothDevice;
22 import android.bluetooth.BluetoothProfile;
23 import android.content.Context;
24 import android.text.Html;
25 import android.util.Log;
26 
27 import androidx.annotation.Nullable;
28 
29 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
30 import com.android.settingslib.bluetooth.LocalBluetoothManager;
31 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
32 import com.android.tv.settings.R;
33 
34 import java.util.Arrays;
35 import java.util.Collections;
36 import java.util.List;
37 import java.util.concurrent.ExecutionException;
38 import java.util.concurrent.FutureTask;
39 
40 /** Provide utilities for Remote & Accessories. */
41 final class AccessoryUtils {
42 
43     public static final String TAG = "AccessoryUtils";
44 
45     private static final int MINOR_MASK = 0b11111100;
46     // Includes any generic keyboards or pointers, and any joystick, game pad, or remote subtypes.
47     private static final int MINOR_REMOTE_MASK = 0b11001100;
48     private static List<String> sKnownDeviceLabels = null;
49 
50     /** This allows OEM to easily override the main Service if desired. */
getBluetoothDeviceServiceClass()51     public static Class getBluetoothDeviceServiceClass() {
52         return BluetoothDevicesService.class;
53     }
54 
getLocalBluetoothManager(Context context)55     public static LocalBluetoothManager getLocalBluetoothManager(Context context) {
56         final FutureTask<LocalBluetoothManager> localBluetoothManagerFutureTask =
57                 new FutureTask<>(
58                         // Avoid StrictMode ThreadPolicy violation
59                         () -> LocalBluetoothManager.getInstance(
60                                 context, (c, bluetoothManager) -> {
61                                 })
62                 );
63         try {
64             localBluetoothManagerFutureTask.run();
65             return localBluetoothManagerFutureTask.get();
66         } catch (InterruptedException | ExecutionException e) {
67             Log.w(TAG, "Error getting LocalBluetoothManager.", e);
68             return null;
69         }
70     }
71 
getCachedBluetoothDevice( Context context, BluetoothDevice device)72     public static CachedBluetoothDevice getCachedBluetoothDevice(
73             Context context, BluetoothDevice device) {
74         LocalBluetoothManager localBluetoothManager = getLocalBluetoothManager(context);
75         if (localBluetoothManager != null) {
76             return localBluetoothManager.getCachedDeviceManager().findDevice(device);
77         }
78         return null;
79     }
80 
getDefaultBluetoothAdapter()81     public static BluetoothAdapter getDefaultBluetoothAdapter() {
82         final FutureTask<BluetoothAdapter> defaultBluetoothAdapterFutureTask =
83                 new FutureTask<>(
84                         // Avoid StrictMode ThreadPolicy violation
85                         BluetoothAdapter::getDefaultAdapter);
86         try {
87             defaultBluetoothAdapterFutureTask.run();
88             return defaultBluetoothAdapterFutureTask.get();
89         } catch (InterruptedException | ExecutionException e) {
90             Log.w(TAG, "Error getting default BluetoothAdapter.", e);
91             return null;
92         }
93     }
94 
getLocalName(BluetoothDevice device)95     public static String getLocalName(BluetoothDevice device) {
96         if (device == null) {
97             return null;
98         }
99         return device.getAlias();
100     }
101 
isBluetoothEnabled()102     public static boolean isBluetoothEnabled() {
103         return getDefaultBluetoothAdapter() != null && getDefaultBluetoothAdapter().isEnabled();
104     }
105 
isConnected(BluetoothDevice device)106     public static boolean isConnected(BluetoothDevice device) {
107         if (device == null) {
108             return false;
109         }
110         return device.getBondState() == BluetoothDevice.BOND_BONDED && device.isConnected();
111     }
112 
isBonded(BluetoothDevice device)113     public static boolean isBonded(BluetoothDevice device) {
114         if (device == null) {
115             return false;
116         }
117         return device.getBondState() == BluetoothDevice.BOND_BONDED && !device.isConnected();
118     }
119 
isRemoteClass(BluetoothDevice device)120     public static boolean isRemoteClass(BluetoothDevice device) {
121         if (device == null || device.getBluetoothClass() == null) {
122             return false;
123         }
124         int major = device.getBluetoothClass().getMajorDeviceClass();
125         int minor = device.getBluetoothClass().getDeviceClass() & MINOR_MASK;
126         return BluetoothClass.Device.Major.PERIPHERAL == major
127                 && (minor & ~MINOR_REMOTE_MASK) == 0;
128     }
129 
130     // For partner, this will be used to identify official device to omit it in the generic
131     // accessories section since the device's settings will be displayed in partner-implemented
132     // Slice.
isKnownDevice(Context context, BluetoothDevice device)133     public static boolean isKnownDevice(Context context, BluetoothDevice device) {
134         if (device == null || device.getName() == null) {
135             return false;
136         }
137         if (sKnownDeviceLabels == null) {
138             if (context == null) {
139                 return false;
140             } else {
141                 sKnownDeviceLabels =
142                         Collections.unmodifiableList(
143                                 Arrays.asList(context.getResources().getStringArray(
144                                         R.array.known_bluetooth_device_labels)));
145                 // For backward compatibility, the customization name used to be known_remote_labels
146                 if (sKnownDeviceLabels.isEmpty()) {
147                     sKnownDeviceLabels = Collections.unmodifiableList(
148                             Arrays.asList(
149                                 context.getResources().getStringArray(
150                                     R.array.known_remote_labels)));
151                 }
152             }
153         }
154 
155         final String name = device.getName().toLowerCase();
156         for (String knownLabel : sKnownDeviceLabels) {
157             if (name.contains(knownLabel.toLowerCase())) {
158                 return true;
159             }
160         }
161         return false;
162     }
163 
164     @Nullable
getHtmlEscapedDeviceName(@ullable BluetoothDevice bluetoothDevice)165     static String getHtmlEscapedDeviceName(@Nullable BluetoothDevice bluetoothDevice) {
166         if (bluetoothDevice == null || bluetoothDevice.getName() == null) {
167             return null;
168         }
169         return Html.escapeHtml(bluetoothDevice.getName());
170     }
171 
isBluetoothHeadset(BluetoothDevice device)172     public static boolean isBluetoothHeadset(BluetoothDevice device) {
173         if (device == null) {
174             return false;
175         }
176         final BluetoothClass bluetoothClass = device.getBluetoothClass();
177         final int devClass = bluetoothClass.getDeviceClass();
178         return (devClass == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET
179                 || devClass == BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES
180                 || devClass == BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER
181                 || devClass == BluetoothClass.Device.AUDIO_VIDEO_PORTABLE_AUDIO
182                 || devClass == BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO);
183     }
184 
isA2dpSource(BluetoothDevice device)185     static boolean isA2dpSource(BluetoothDevice device) {
186         return device != null && device.getBluetoothClass() != null
187                 && device.getBluetoothClass().doesClassMatch(BluetoothProfile.A2DP);
188     }
189 
190     /** Returns true if the BluetoothDevice is the active audio output, false otherwise. */
isActiveAudioOutput(BluetoothDevice device)191     static boolean isActiveAudioOutput(BluetoothDevice device) {
192         if (device != null) {
193             final BluetoothAdapter btAdapter = getDefaultBluetoothAdapter();
194             if (btAdapter != null) {
195                 return btAdapter.getActiveDevices(BluetoothProfile.A2DP).contains(device);
196             }
197         }
198         return false;
199     }
200 
201     /**
202      * Sets the specified BluetoothDevice as the active audio output. Passing `null`
203      * resets the active audio output to the default. Returns false on immediate error,
204      * true otherwise.
205      */
setActiveAudioOutput(BluetoothDevice device)206     static boolean setActiveAudioOutput(BluetoothDevice device) {
207         // null is an accepted value for unsetting the active audio output
208         final BluetoothAdapter btAdapter = getDefaultBluetoothAdapter();
209         if (btAdapter != null) {
210             if (device == null) {
211                 return btAdapter.removeActiveDevice(BluetoothAdapter.ACTIVE_DEVICE_AUDIO);
212             } else {
213                 return btAdapter.setActiveDevice(device, BluetoothAdapter.ACTIVE_DEVICE_AUDIO);
214             }
215         }
216         return false;
217     }
218 
219     /**
220      * Returns true if the CachedBluetoothDevice supports an audio profile (A2DP for now),
221      * false otherwise.
222      */
hasAudioProfile(CachedBluetoothDevice cachedDevice)223     public static boolean hasAudioProfile(CachedBluetoothDevice cachedDevice) {
224         if (cachedDevice != null) {
225             for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
226                 if (profile.getProfileId() == BluetoothProfile.A2DP) {
227                     return true;
228                 }
229             }
230         }
231         return false;
232     }
233 
AccessoryUtils()234     private AccessoryUtils() {
235         // do not allow instantiation
236     }
237 }
238