1 /* 2 * Copyright (C) 2017 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.server.cts.device.batterystats; 18 19 import android.accounts.Account; 20 import android.app.Activity; 21 import android.app.ActivityManager; 22 import android.bluetooth.BluetoothAdapter; 23 import android.bluetooth.le.BluetoothLeScanner; 24 import android.bluetooth.le.ScanCallback; 25 import android.bluetooth.le.ScanResult; 26 import android.bluetooth.le.ScanSettings; 27 import android.content.BroadcastReceiver; 28 import android.app.job.JobInfo; 29 import android.app.job.JobScheduler; 30 import android.content.ComponentName; 31 import android.content.ContentResolver; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.content.IntentFilter; 35 import android.graphics.Color; 36 import android.graphics.Point; 37 import android.location.Location; 38 import android.location.LocationListener; 39 import android.location.LocationManager; 40 import android.os.AsyncTask; 41 import android.os.Bundle; 42 import android.os.Handler; 43 import android.os.HandlerThread; 44 import android.os.Looper; 45 import android.util.Log; 46 import android.view.Gravity; 47 import android.view.View; 48 import android.view.ViewGroup; 49 import android.view.WindowManager; 50 51 52 import org.junit.Assert; 53 54 import java.util.List; 55 import java.util.concurrent.CountDownLatch; 56 import java.util.concurrent.TimeUnit; 57 58 public class BatteryStatsBgVsFgActions { 59 private static final String TAG = BatteryStatsBgVsFgActions.class.getSimpleName(); 60 61 private static final int DO_NOTHING_TIMEOUT = 2000; 62 63 public static final String KEY_ACTION = "action"; 64 public static final String ACTION_BLE_SCAN_OPTIMIZED = "action.ble_scan_optimized"; 65 public static final String ACTION_BLE_SCAN_UNOPTIMIZED = "action.ble_scan_unoptimized"; 66 public static final String ACTION_GPS = "action.gps"; 67 public static final String ACTION_JOB_SCHEDULE = "action.jobs"; 68 public static final String ACTION_SYNC = "action.sync"; 69 public static final String ACTION_SLEEP_WHILE_BACKGROUND = "action.sleep_background"; 70 public static final String ACTION_SLEEP_WHILE_TOP = "action.sleep_top"; 71 public static final String ACTION_SHOW_APPLICATION_OVERLAY = "action.show_application_overlay"; 72 73 public static final String KEY_REQUEST_CODE = "request_code"; 74 75 /** Number of times to check that app is in correct state before giving up. */ 76 public static final int PROC_STATE_CHECK_ATTEMPTS = 10; 77 78 /** Number of times to check that Bluetooth is enabled before giving up. */ 79 public static final int BT_ENABLE_ATTEMPTS = 8; 80 81 /** Perform the action specified by the given action code (see constants above). */ doAction(Context ctx, String actionCode, String requestCode)82 public static void doAction(Context ctx, String actionCode, String requestCode) { 83 if (actionCode == null) { 84 Log.e(TAG, "Intent was missing action."); 85 return; 86 } 87 sleep(100); 88 switch (actionCode) { 89 case ACTION_BLE_SCAN_OPTIMIZED: 90 doOptimizedBleScan(ctx, requestCode); 91 break; 92 case ACTION_BLE_SCAN_UNOPTIMIZED: 93 doUnoptimizedBleScan(ctx, requestCode); 94 break; 95 case ACTION_GPS: 96 doGpsUpdate(ctx, requestCode); 97 break; 98 case ACTION_JOB_SCHEDULE: 99 doScheduleJob(ctx, requestCode); 100 break; 101 case ACTION_SYNC: 102 doSync(ctx, requestCode); 103 break; 104 case ACTION_SLEEP_WHILE_BACKGROUND: 105 sleep(DO_NOTHING_TIMEOUT); 106 tellHostActionFinished(ACTION_SLEEP_WHILE_BACKGROUND, requestCode); 107 break; 108 case ACTION_SLEEP_WHILE_TOP: 109 doNothingAsync(ctx, ACTION_SLEEP_WHILE_TOP, requestCode); 110 break; 111 case ACTION_SHOW_APPLICATION_OVERLAY: 112 showApplicationOverlay(ctx, requestCode); 113 break; 114 default: 115 Log.e(TAG, "Intent had invalid action"); 116 } 117 sleep(100); 118 } 119 showApplicationOverlay(Context ctx, String requestCode)120 private static void showApplicationOverlay(Context ctx, String requestCode) { 121 final WindowManager wm = ctx.getSystemService(WindowManager.class); 122 Point size = new Point(); 123 wm.getDefaultDisplay().getSize(size); 124 125 WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams( 126 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, 127 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 128 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON 129 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); 130 wmlp.width = size.x / 4; 131 wmlp.height = size.y / 4; 132 wmlp.gravity = Gravity.CENTER | Gravity.LEFT; 133 wmlp.setTitle(ctx.getPackageName()); 134 135 ViewGroup.LayoutParams vglp = new ViewGroup.LayoutParams( 136 ViewGroup.LayoutParams.MATCH_PARENT, 137 ViewGroup.LayoutParams.MATCH_PARENT); 138 139 View v = new View(ctx); 140 v.setBackgroundColor(Color.GREEN); 141 v.setLayoutParams(vglp); 142 wm.addView(v, wmlp); 143 144 tellHostActionFinished(ACTION_SHOW_APPLICATION_OVERLAY, requestCode); 145 } 146 doOptimizedBleScan(Context ctx, String requestCode)147 private static void doOptimizedBleScan(Context ctx, String requestCode) { 148 ScanSettings scanSettings = new ScanSettings.Builder() 149 .setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC).build(); 150 performBleScan(scanSettings); 151 tellHostActionFinished(ACTION_BLE_SCAN_OPTIMIZED, requestCode); 152 } 153 doUnoptimizedBleScan(Context ctx, String requestCode)154 private static void doUnoptimizedBleScan(Context ctx, String requestCode) { 155 ScanSettings scanSettings = new ScanSettings.Builder() 156 .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(); 157 performBleScan(scanSettings); 158 tellHostActionFinished(ACTION_BLE_SCAN_UNOPTIMIZED, requestCode); 159 } 160 performBleScan(ScanSettings scanSettings)161 private static void performBleScan(ScanSettings scanSettings) { 162 BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 163 if (bluetoothAdapter == null) { 164 Log.e(TAG, "Device does not support Bluetooth"); 165 return; 166 } 167 boolean bluetoothEnabledByTest = false; 168 if (!bluetoothAdapter.isEnabled()) { 169 if (!bluetoothAdapter.enable()) { 170 Log.e(TAG, "Bluetooth is not enabled"); 171 return; 172 } 173 for (int attempt = 0; attempt < BT_ENABLE_ATTEMPTS; attempt++) { 174 if (bluetoothAdapter.isEnabled()) { 175 break; 176 } else { 177 if (attempt < BT_ENABLE_ATTEMPTS - 1) { 178 sleep(1_000); 179 } else { 180 throw new RuntimeException("Bluetooth enable failed."); 181 } 182 } 183 } 184 bluetoothEnabledByTest = true; 185 } 186 187 BluetoothLeScanner bleScanner = bluetoothAdapter.getBluetoothLeScanner(); 188 if (bleScanner == null) { 189 Log.e(TAG, "Cannot access BLE scanner"); 190 return; 191 } 192 193 ScanCallback scanCallback = new ScanCallback() { 194 @Override 195 public void onScanResult(int callbackType, ScanResult result) { 196 Log.v(TAG, "called onScanResult"); 197 } 198 199 @Override 200 public void onScanFailed(int errorCode) { 201 Log.v(TAG, "called onScanFailed"); 202 } 203 204 @Override 205 public void onBatchScanResults(List<ScanResult> results) { 206 Log.v(TAG, "called onBatchScanResults"); 207 } 208 }; 209 210 bleScanner.startScan(null, scanSettings, scanCallback); 211 sleep(2_000); 212 bleScanner.stopScan(scanCallback); 213 214 // Restore adapter state at end of test 215 if (bluetoothEnabledByTest) { 216 bluetoothAdapter.disable(); 217 } 218 } 219 doGpsUpdate(Context ctx, String requestCode)220 private static void doGpsUpdate(Context ctx, String requestCode) { 221 final LocationManager locManager = ctx.getSystemService(LocationManager.class); 222 if (!locManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { 223 Log.e(TAG, "GPS provider is not enabled"); 224 tellHostActionFinished(ACTION_GPS, requestCode); 225 return; 226 } 227 CountDownLatch latch = new CountDownLatch(1); 228 229 final LocationListener locListener = new LocationListener() { 230 public void onLocationChanged(Location location) { 231 Log.v(TAG, "onLocationChanged: location has been obtained"); 232 } 233 234 public void onProviderDisabled(String provider) { 235 Log.w(TAG, "onProviderDisabled " + provider); 236 } 237 238 public void onProviderEnabled(String provider) { 239 Log.w(TAG, "onProviderEnabled " + provider); 240 } 241 242 public void onStatusChanged(String provider, int status, Bundle extras) { 243 Log.w(TAG, "onStatusChanged " + provider + " " + status); 244 } 245 }; 246 247 HandlerThread handlerThread = new HandlerThread("doGpsUpdate_bg"); 248 handlerThread.start(); 249 250 Handler handler = new Handler(handlerThread.getLooper()); 251 handler.post(() -> { 252 locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 990, 0, 253 locListener); 254 sleep(1_000); 255 locManager.removeUpdates(locListener); 256 latch.countDown(); 257 }); 258 259 waitForReceiver(ctx, 59_000, latch, null); 260 handlerThread.quitSafely(); 261 262 tellHostActionFinished(ACTION_GPS, requestCode); 263 } 264 doScheduleJob(Context ctx, String requestCode)265 private static void doScheduleJob(Context ctx, String requestCode) { 266 final ComponentName JOB_COMPONENT_NAME = 267 new ComponentName("com.android.server.cts.device.batterystats", 268 SimpleJobService.class.getName()); 269 JobScheduler js = ctx.getSystemService(JobScheduler.class); 270 if (js == null) { 271 Log.e(TAG, "JobScheduler service not available"); 272 tellHostActionFinished(ACTION_JOB_SCHEDULE, requestCode); 273 return; 274 } 275 final JobInfo job = (new JobInfo.Builder(1, JOB_COMPONENT_NAME)) 276 .setOverrideDeadline(0) 277 .build(); 278 CountDownLatch latch = SimpleJobService.resetCountDownLatch(); 279 js.schedule(job); 280 // Job starts in main thread so wait in another thread to see if job finishes. 281 new AsyncTask<Void, Void, Void>() { 282 @Override 283 protected Void doInBackground(Void... params) { 284 waitForReceiver(null, 60_000, latch, null); 285 tellHostActionFinished(ACTION_JOB_SCHEDULE, requestCode); 286 return null; 287 } 288 }.execute(); 289 } 290 doNothingAsync(Context ctx, String requestCode, String actionCode)291 private static void doNothingAsync(Context ctx, String requestCode, String actionCode) { 292 new AsyncTask<Void, Void, Void>() { 293 @Override 294 protected Void doInBackground(Void... params) { 295 sleep(DO_NOTHING_TIMEOUT); 296 return null; 297 } 298 299 @Override 300 protected void onPostExecute(Void nothing) { 301 if (ctx instanceof Activity) { 302 ((Activity) ctx).finish(); 303 tellHostActionFinished(actionCode, requestCode); 304 } 305 } 306 }.execute(); 307 } 308 doSync(Context ctx, String requestCode)309 private static void doSync(Context ctx, String requestCode) { 310 BatteryStatsAuthenticator.removeAllAccounts(ctx); 311 final Account account = BatteryStatsAuthenticator.getTestAccount(); 312 // Create the test account. 313 BatteryStatsAuthenticator.ensureTestAccount(ctx); 314 // Force set is syncable. 315 ContentResolver.setMasterSyncAutomatically(true); 316 ContentResolver.setIsSyncable(account, BatteryStatsProvider.AUTHORITY, 1); 317 318 new AsyncTask<Void, Void, Void>() { 319 @Override 320 protected Void doInBackground(Void... params) { 321 try { 322 Log.v(TAG, "Starting sync"); 323 BatteryStatsSyncAdapter.requestSync(account); 324 sleep(500); 325 } catch (Exception e) { 326 Log.e(TAG, "Exception trying to sync", e); 327 } 328 BatteryStatsAuthenticator.removeAllAccounts(ctx); 329 return null; 330 } 331 332 @Override 333 protected void onPostExecute(Void aVoid) { 334 super.onPostExecute(aVoid); 335 Log.v(TAG, "Finished sync method"); 336 // If ctx is an Activity, finish it when sync is done. If it's a service, don't. 337 if (ctx instanceof Activity) { 338 ((Activity) ctx).finish(); 339 } 340 tellHostActionFinished(ACTION_SYNC, requestCode); 341 } 342 }.execute(); 343 } 344 345 /** Register receiver to determine when given action is complete. */ registerReceiver( Context ctx, CountDownLatch onReceiveLatch, IntentFilter intentFilter)346 private static BroadcastReceiver registerReceiver( 347 Context ctx, CountDownLatch onReceiveLatch, IntentFilter intentFilter) { 348 BroadcastReceiver receiver = new BroadcastReceiver() { 349 @Override 350 public void onReceive(Context context, Intent intent) { 351 onReceiveLatch.countDown(); 352 } 353 }; 354 // run Broadcast receiver in a different thread since the foreground activity will wait. 355 HandlerThread handlerThread = new HandlerThread("br_handler_thread"); 356 handlerThread.start(); 357 Looper looper = handlerThread.getLooper(); 358 Handler handler = new Handler(looper); 359 ctx.registerReceiver(receiver, intentFilter, null, handler); 360 return receiver; 361 } 362 363 /** 364 * Uses the receiver to wait until the action is complete. ctx and receiver may be null if no 365 * receiver is needed to be unregistered. 366 */ waitForReceiver(Context ctx, int maxWaitTimeMs, CountDownLatch latch, BroadcastReceiver receiver)367 private static void waitForReceiver(Context ctx, 368 int maxWaitTimeMs, CountDownLatch latch, BroadcastReceiver receiver) { 369 try { 370 boolean didFinish = latch.await(maxWaitTimeMs, TimeUnit.MILLISECONDS); 371 if (didFinish) { 372 Log.v(TAG, "Finished performing action"); 373 } else { 374 // This is not necessarily a problem. If we just want to make sure a count was 375 // recorded for the request, it doesn't matter if the action actually finished. 376 Log.w(TAG, "Did not finish in specified time."); 377 } 378 } catch (InterruptedException e) { 379 Log.e(TAG, "Interrupted exception while awaiting action to finish", e); 380 } 381 if (ctx != null && receiver != null) { 382 ctx.unregisterReceiver(receiver); 383 } 384 } 385 386 /** Communicates to hostside (via logcat) that action has completed (regardless of success). */ tellHostActionFinished(String actionCode, String requestCode)387 private static void tellHostActionFinished(String actionCode, String requestCode) { 388 String s = String.format("Completed performing %s for request %s", actionCode, requestCode); 389 Log.i(TAG, s); 390 } 391 392 /** Determines whether the package is running as a background process. */ isAppInBackground(Context context)393 private static boolean isAppInBackground(Context context) throws ReflectiveOperationException { 394 String pkgName = context.getPackageName(); 395 ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 396 List<ActivityManager.RunningAppProcessInfo> processes = am.getRunningAppProcesses(); 397 if (processes == null) { 398 return false; 399 } 400 for (ActivityManager.RunningAppProcessInfo r : processes) { 401 // BatteryStatsImpl treats as background if procState is >= 402 // Activitymanager.PROCESS_STATE_IMPORTANT_BACKGROUND (corresponding 403 // to BatteryStats.PROCESS_STATE_BACKGROUND). 404 // Due to lack of permissions, only the current app should show up in the list of 405 // processes, which is desired in this case; but in case this changes later, we check 406 // that the package name matches anyway. 407 int processState = -1; 408 int backgroundCode = -1; 409 try { 410 processState = ActivityManager.RunningAppProcessInfo.class 411 .getField("processState").getInt(r); 412 backgroundCode = (Integer) ActivityManager.class 413 .getDeclaredField("PROCESS_STATE_IMPORTANT_BACKGROUND").get(null); 414 } catch (ReflectiveOperationException ex) { 415 Log.e(TAG, "Failed to get proc state info via reflection", ex); 416 throw ex; 417 } 418 if (processState < backgroundCode) { // if foreground process 419 for (String rpkg : r.pkgList) { 420 if (pkgName.equals(rpkg)) { 421 return false; 422 } 423 } 424 } 425 } 426 return true; 427 } 428 429 /** 430 * Makes sure app is in desired state, either background (if shouldBeBg = true) or foreground 431 * (if shouldBeBg = false). 432 * Tries for up to PROC_STATE_CHECK_ATTEMPTS seconds. If app is still not in the correct state, 433 * throws an AssertionError failure to crash the app. 434 */ checkAppState( Context context, boolean shouldBeBg, String actionCode, String requestCode)435 public static void checkAppState( 436 Context context, boolean shouldBeBg, String actionCode, String requestCode) { 437 final String errMsg = "App is " + (shouldBeBg ? "not " : "") + "a background process!"; 438 try { 439 for (int attempt = 0; attempt < PROC_STATE_CHECK_ATTEMPTS; attempt++) { 440 if (shouldBeBg == isAppInBackground(context)) { 441 return; // No problems. 442 } else { 443 if (attempt < PROC_STATE_CHECK_ATTEMPTS - 1) { 444 Log.w(TAG, errMsg + " Trying again in 1s."); 445 sleep(1_000); 446 } else { 447 Log.e(TAG, errMsg + " Quiting app."); 448 BatteryStatsBgVsFgActions.tellHostActionFinished(actionCode, requestCode); 449 Assert.fail(errMsg + " Test requires app to be in the correct state."); 450 } 451 } 452 } 453 } catch(ReflectiveOperationException ex) { 454 Log.w(TAG, "Couldn't determine if app is in background. Proceeding with test anyway."); 455 } 456 } 457 458 /** Puts the current thread to sleep. */ sleep(int millis)459 private static void sleep(int millis) { 460 try { 461 Thread.sleep(millis); 462 } catch (InterruptedException e) { 463 Log.e(TAG, "Interrupted exception while sleeping", e); 464 } 465 } 466 } 467