1 /*
2  * Copyright (C) 2020 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 android.bluetooth.test_utils;
18 
19 import android.bluetooth.BluetoothAdapter;
20 import android.bluetooth.BluetoothProfile;
21 import android.bluetooth.le.ScanRecord;
22 import android.content.Context;
23 import android.content.pm.PackageManager;
24 import android.provider.Settings;
25 import android.util.Log;
26 
27 import androidx.annotation.Nullable;
28 import androidx.test.platform.app.InstrumentationRegistry;
29 
30 import com.google.errorprone.annotations.InlineMe;
31 
32 import java.lang.reflect.InvocationTargetException;
33 import java.lang.reflect.Method;
34 import java.time.Duration;
35 import java.util.Objects;
36 import java.util.Set;
37 import java.util.concurrent.TimeUnit;
38 import java.util.concurrent.locks.Condition;
39 import java.util.concurrent.locks.ReentrantLock;
40 
41 /** Utility class for Bluetooth CTS test. */
42 public class TestUtils {
43     protected static final String TAG = TestUtils.class.getSimpleName();
44 
45     /**
46      * Checks whether this device has Bluetooth feature
47      *
48      * @return true if this device has Bluetooth feature
49      */
hasBluetooth()50     public static boolean hasBluetooth() {
51         Context context = InstrumentationRegistry.getInstrumentation().getContext();
52         return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
53     }
54 
55     /**
56      * Adopt shell UID's permission via {@link android.app.UiAutomation}
57      *
58      * @param permission permission to adopt
59      */
adoptPermissionAsShellUid(@ullable String... permission)60     public static void adoptPermissionAsShellUid(@Nullable String... permission) {
61         InstrumentationRegistry.getInstrumentation()
62                 .getUiAutomation()
63                 .adoptShellPermissionIdentity(permission);
64     }
65 
66     /** Drop all permissions adopted as shell UID */
dropPermissionAsShellUid()67     public static void dropPermissionAsShellUid() {
68         InstrumentationRegistry.getInstrumentation()
69                 .getUiAutomation()
70                 .dropShellPermissionIdentity();
71     }
72 
73     /**
74      * @return permissions adopted from Shell on this process
75      */
getAdoptedShellPermissions()76     public static Set<String> getAdoptedShellPermissions() {
77         return InstrumentationRegistry.getInstrumentation()
78                 .getUiAutomation()
79                 .getAdoptedShellPermissions();
80     }
81 
82     /**
83      * Utility method to call hidden ScanRecord.parseFromBytes method.
84      *
85      * @param bytes Raw bytes from BLE payload
86      * @return parsed {@link ScanRecord}, null if parsing failed
87      */
parseScanRecord(byte[] bytes)88     public static ScanRecord parseScanRecord(byte[] bytes) {
89         Class<?> scanRecordClass = ScanRecord.class;
90         try {
91             Method method = scanRecordClass.getDeclaredMethod("parseFromBytes", byte[].class);
92             return (ScanRecord) method.invoke(null, bytes);
93         } catch (NoSuchMethodException
94                 | IllegalAccessException
95                 | IllegalArgumentException
96                 | InvocationTargetException e) {
97             return null;
98         }
99     }
100 
101     /**
102      * Get current location mode settings.
103      *
104      * @param context current running context
105      * @return values among {@link Settings.Secure#LOCATION_MODE_OFF}, {@link
106      *     Settings.Secure#LOCATION_MODE_ON}, {@link Settings.Secure#LOCATION_MODE_SENSORS_ONLY},
107      *     {@link Settings.Secure#LOCATION_MODE_HIGH_ACCURACY}, {@link
108      *     Settings.Secure#LOCATION_MODE_BATTERY_SAVING}
109      */
getLocationMode(Context context)110     public static int getLocationMode(Context context) {
111         return Settings.Secure.getInt(
112                 context.getContentResolver(),
113                 Settings.Secure.LOCATION_MODE,
114                 Settings.Secure.LOCATION_MODE_OFF);
115     }
116 
117     /**
118      * Set location settings mode.
119      *
120      * @param context current running context
121      * @param mode a value for {@link Settings.Secure#LOCATION_MODE} among {@link
122      *     Settings.Secure#LOCATION_MODE_OFF}, {@link Settings.Secure#LOCATION_MODE_ON}, {@link
123      *     Settings.Secure#LOCATION_MODE_SENSORS_ONLY}, {@link
124      *     Settings.Secure#LOCATION_MODE_HIGH_ACCURACY}, {@link
125      *     Settings.Secure#LOCATION_MODE_BATTERY_SAVING}
126      */
setLocationMode(Context context, int mode)127     public static void setLocationMode(Context context, int mode) {
128         Settings.Secure.putInt(context.getContentResolver(), Settings.Secure.LOCATION_MODE, mode);
129     }
130 
131     /**
132      * Return true if location is on.
133      *
134      * @param context current running context
135      * @return true if location mode is in one of the enabled value
136      */
isLocationOn(Context context)137     public static boolean isLocationOn(Context context) {
138         return getLocationMode(context) != Settings.Secure.LOCATION_MODE_OFF;
139     }
140 
141     /** Enable location and set the mode to GPS only. */
enableLocation(Context context)142     public static void enableLocation(Context context) {
143         setLocationMode(context, Settings.Secure.LOCATION_MODE_SENSORS_ONLY);
144     }
145 
146     /**
147      * Disable location by setting is to {@link Settings.Secure#LOCATION_MODE_OFF}
148      *
149      * @param context current running context
150      */
disableLocation(Context context)151     public static void disableLocation(Context context) {
152         setLocationMode(context, Settings.Secure.LOCATION_MODE_OFF);
153     }
154 
155     /**
156      * Check if BLE is supported by this platform
157      *
158      * @param context current device context
159      * @return true if BLE is supported, false otherwise
160      */
isBleSupported(Context context)161     public static boolean isBleSupported(Context context) {
162         return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
163     }
164 
165     /**
166      * Check if this is an automotive device
167      *
168      * @param context current device context
169      * @return true if this Android device is an automotive device, false otherwise
170      */
isAutomotive(Context context)171     public static boolean isAutomotive(Context context) {
172         return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
173     }
174 
175     /**
176      * Check if this is a watch device
177      *
178      * @param context current device context
179      * @return true if this Android device is a watch device, false otherwise
180      */
isWatch(Context context)181     public static boolean isWatch(Context context) {
182         return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
183     }
184 
185     /**
186      * Check if this is a TV device
187      *
188      * @param context current device context
189      * @return true if this Android device is a TV device, false otherwise
190      */
isTv(Context context)191     public static boolean isTv(Context context) {
192         PackageManager pm = context.getPackageManager();
193         return pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION)
194                 || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK);
195     }
196 
197     /** Boilerplate class for profile listener */
198     public static class BluetoothCtsServiceConnector {
199         // Timeout for Proxy Connect
200         private static final Duration PROXY_CONNECTION_TIMEOUT = Duration.ofMillis(500);
201         private BluetoothProfile mProfileProxy = null;
202         private boolean mIsProfileReady = false;
203         private boolean mIsProfileConnecting = false;
204         private final Condition mConditionProfileConnection;
205         private final ReentrantLock mProfileConnectionLock;
206         private final String mLogTag;
207         private final int mProfileId;
208         private final BluetoothAdapter mAdapter;
209         private final Context mContext;
210 
BluetoothCtsServiceConnector( String logTag, int profileId, BluetoothAdapter adapter, Context context)211         public BluetoothCtsServiceConnector(
212                 String logTag, int profileId, BluetoothAdapter adapter, Context context) {
213             mLogTag = Objects.requireNonNull(logTag);
214             mProfileId = profileId;
215             mAdapter = Objects.requireNonNull(adapter);
216             mContext = Objects.requireNonNull(context);
217             mProfileConnectionLock = new ReentrantLock();
218             mConditionProfileConnection = mProfileConnectionLock.newCondition();
219         }
220 
getProfileProxy()221         public BluetoothProfile getProfileProxy() {
222             return mProfileProxy;
223         }
224 
225         /** Close profile proxy */
closeProfileProxy()226         public void closeProfileProxy() {
227             if (mProfileProxy != null) {
228                 mAdapter.closeProfileProxy(mProfileId, mProfileProxy);
229                 mProfileProxy = null;
230                 mIsProfileReady = false;
231             }
232         }
233 
234         /**
235          * Open profile proxy
236          *
237          * @return true if the profile proxy is opened successfully
238          */
openProfileProxyAsync()239         public boolean openProfileProxyAsync() {
240             mIsProfileConnecting = mAdapter.getProfileProxy(mContext, mServiceListener, mProfileId);
241             return mIsProfileConnecting;
242         }
243 
244         /**
245          * Wait for profile service to connect
246          *
247          * @return true if the service is connected on time
248          */
waitForProfileConnect()249         public boolean waitForProfileConnect() {
250             return waitForProfileConnect(PROXY_CONNECTION_TIMEOUT);
251         }
252 
253         /**
254          * Wait for profile service to connect with timeouts
255          *
256          * @param timeoutMs duration of the timeout in milliseconds
257          * @return true if the service is connected on time
258          * @deprecated Please use {@link #waitForProfileConnect(Duration)} instead
259          */
260         @Deprecated
261         @InlineMe(
262                 replacement = "this.waitForProfileConnect(Duration.ofMillis(timeoutMs))",
263                 imports = "java.time.Duration")
waitForProfileConnect(int timeoutMs)264         public final boolean waitForProfileConnect(int timeoutMs) {
265             return waitForProfileConnect(Duration.ofMillis(timeoutMs));
266         }
267 
268         /**
269          * Wait for profile service to connect with timeouts
270          *
271          * @param timeout duration of the timeout
272          * @return true if the service is connected on time
273          */
waitForProfileConnect(Duration timeout)274         public boolean waitForProfileConnect(Duration timeout) {
275             if (!mIsProfileConnecting) {
276                 mIsProfileConnecting =
277                         mAdapter.getProfileProxy(mContext, mServiceListener, mProfileId);
278             }
279             if (!mIsProfileConnecting) {
280                 return false;
281             }
282             mProfileConnectionLock.lock();
283             try {
284                 // Wait for the Adapter to be disabled
285                 while (!mIsProfileReady) {
286                     if (!mConditionProfileConnection.await(
287                             timeout.toMillis(), TimeUnit.MILLISECONDS)) {
288                         // Timeout
289                         Log.e(mLogTag, "Timeout while waiting for Profile Connect");
290                         break;
291                     } // else spurious wake-ups
292                 }
293             } catch (InterruptedException e) {
294                 Log.e(mLogTag, "waitForProfileConnect: interrupted");
295             } finally {
296                 mProfileConnectionLock.unlock();
297             }
298             mIsProfileConnecting = false;
299             return mIsProfileReady;
300         }
301 
302         private final BluetoothProfile.ServiceListener mServiceListener =
303                 new BluetoothProfile.ServiceListener() {
304                     @Override
305                     public void onServiceConnected(int profile, BluetoothProfile proxy) {
306                         mProfileConnectionLock.lock();
307                         try {
308                             mProfileProxy = proxy;
309                             mIsProfileReady = true;
310                             mConditionProfileConnection.signal();
311                         } finally {
312                             mProfileConnectionLock.unlock();
313                         }
314                     }
315 
316                     @Override
317                     public void onServiceDisconnected(int profile) {
318                         mProfileConnectionLock.lock();
319                         try {
320                             mIsProfileReady = false;
321                             mConditionProfileConnection.signal();
322                         } finally {
323                             mProfileConnectionLock.unlock();
324                         }
325                     }
326                 };
327     }
328 }
329