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