| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.cts.device.batterystats; |
| |
| import android.accounts.Account; |
| import android.app.Activity; |
| import android.app.ActivityManager; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.le.BluetoothLeScanner; |
| import android.bluetooth.le.ScanCallback; |
| import android.bluetooth.le.ScanResult; |
| import android.bluetooth.le.ScanSettings; |
| import android.content.BroadcastReceiver; |
| import android.app.job.JobInfo; |
| import android.app.job.JobScheduler; |
| import android.content.ComponentName; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.graphics.Color; |
| import android.graphics.Point; |
| import android.location.Location; |
| import android.location.LocationListener; |
| import android.location.LocationManager; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Looper; |
| import android.util.Log; |
| import android.view.Gravity; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.WindowManager; |
| |
| |
| import org.junit.Assert; |
| |
| import java.util.List; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| |
| public class BatteryStatsBgVsFgActions { |
| private static final String TAG = BatteryStatsBgVsFgActions.class.getSimpleName(); |
| |
| private static final int DO_NOTHING_TIMEOUT = 2000; |
| |
| public static final String KEY_ACTION = "action"; |
| public static final String ACTION_BLE_SCAN_OPTIMIZED = "action.ble_scan_optimized"; |
| public static final String ACTION_BLE_SCAN_UNOPTIMIZED = "action.ble_scan_unoptimized"; |
| public static final String ACTION_JOB_SCHEDULE = "action.jobs"; |
| public static final String ACTION_SYNC = "action.sync"; |
| public static final String ACTION_SLEEP_WHILE_BACKGROUND = "action.sleep_background"; |
| public static final String ACTION_SLEEP_WHILE_TOP = "action.sleep_top"; |
| public static final String ACTION_SHOW_APPLICATION_OVERLAY = "action.show_application_overlay"; |
| |
| public static final String KEY_REQUEST_CODE = "request_code"; |
| |
| /** Number of times to check that app is in correct state before giving up. */ |
| public static final int PROC_STATE_CHECK_ATTEMPTS = 10; |
| |
| /** Number of times to check that Bluetooth is enabled before giving up. */ |
| public static final int BT_ENABLE_ATTEMPTS = 8; |
| |
| /** Perform the action specified by the given action code (see constants above). */ |
| public static void doAction(Context ctx, String actionCode, String requestCode) { |
| if (actionCode == null) { |
| Log.e(TAG, "Intent was missing action."); |
| return; |
| } |
| sleep(100); |
| switch (actionCode) { |
| case ACTION_BLE_SCAN_OPTIMIZED: |
| doOptimizedBleScan(ctx, requestCode); |
| break; |
| case ACTION_BLE_SCAN_UNOPTIMIZED: |
| doUnoptimizedBleScan(ctx, requestCode); |
| break; |
| case ACTION_JOB_SCHEDULE: |
| doScheduleJob(ctx, requestCode); |
| break; |
| case ACTION_SYNC: |
| doSync(ctx, requestCode); |
| break; |
| case ACTION_SLEEP_WHILE_BACKGROUND: |
| sleep(DO_NOTHING_TIMEOUT); |
| tellHostActionFinished(ACTION_SLEEP_WHILE_BACKGROUND, requestCode); |
| break; |
| case ACTION_SLEEP_WHILE_TOP: |
| doNothingAsync(ctx, ACTION_SLEEP_WHILE_TOP, requestCode); |
| break; |
| case ACTION_SHOW_APPLICATION_OVERLAY: |
| showApplicationOverlay(ctx, requestCode); |
| break; |
| default: |
| Log.e(TAG, "Intent had invalid action"); |
| } |
| sleep(100); |
| } |
| |
| private static void showApplicationOverlay(Context ctx, String requestCode) { |
| final WindowManager wm = ctx.getSystemService(WindowManager.class); |
| Point size = new Point(); |
| wm.getDefaultDisplay().getSize(size); |
| |
| WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams( |
| WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, |
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
| | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
| | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); |
| wmlp.width = size.x / 4; |
| wmlp.height = size.y / 4; |
| wmlp.gravity = Gravity.CENTER | Gravity.LEFT; |
| wmlp.setTitle(ctx.getPackageName()); |
| |
| ViewGroup.LayoutParams vglp = new ViewGroup.LayoutParams( |
| ViewGroup.LayoutParams.MATCH_PARENT, |
| ViewGroup.LayoutParams.MATCH_PARENT); |
| |
| View v = new View(ctx); |
| v.setBackgroundColor(Color.GREEN); |
| v.setLayoutParams(vglp); |
| wm.addView(v, wmlp); |
| |
| tellHostActionFinished(ACTION_SHOW_APPLICATION_OVERLAY, requestCode); |
| } |
| |
| private static void doOptimizedBleScan(Context ctx, String requestCode) { |
| ScanSettings scanSettings = new ScanSettings.Builder() |
| .setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC).build(); |
| performBleScan(scanSettings); |
| tellHostActionFinished(ACTION_BLE_SCAN_OPTIMIZED, requestCode); |
| } |
| |
| private static void doUnoptimizedBleScan(Context ctx, String requestCode) { |
| ScanSettings scanSettings = new ScanSettings.Builder() |
| .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(); |
| performBleScan(scanSettings); |
| tellHostActionFinished(ACTION_BLE_SCAN_UNOPTIMIZED, requestCode); |
| } |
| |
| private static void performBleScan(ScanSettings scanSettings) { |
| BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); |
| if (bluetoothAdapter == null) { |
| Log.e(TAG, "Device does not support Bluetooth"); |
| return; |
| } |
| boolean bluetoothEnabledByTest = false; |
| if (!bluetoothAdapter.isEnabled()) { |
| if (!bluetoothAdapter.enable()) { |
| Log.e(TAG, "Bluetooth is not enabled"); |
| return; |
| } |
| for (int attempt = 0; attempt < BT_ENABLE_ATTEMPTS; attempt++) { |
| if (bluetoothAdapter.isEnabled()) { |
| break; |
| } else { |
| if (attempt < BT_ENABLE_ATTEMPTS - 1) { |
| sleep(1_000); |
| } else { |
| throw new RuntimeException("Bluetooth enable failed."); |
| } |
| } |
| } |
| bluetoothEnabledByTest = true; |
| } |
| |
| BluetoothLeScanner bleScanner = bluetoothAdapter.getBluetoothLeScanner(); |
| if (bleScanner == null) { |
| Log.e(TAG, "Cannot access BLE scanner"); |
| return; |
| } |
| |
| ScanCallback scanCallback = new ScanCallback() { |
| @Override |
| public void onScanResult(int callbackType, ScanResult result) { |
| Log.v(TAG, "called onScanResult"); |
| } |
| |
| @Override |
| public void onScanFailed(int errorCode) { |
| Log.v(TAG, "called onScanFailed"); |
| } |
| |
| @Override |
| public void onBatchScanResults(List<ScanResult> results) { |
| Log.v(TAG, "called onBatchScanResults"); |
| } |
| }; |
| |
| bleScanner.startScan(null, scanSettings, scanCallback); |
| sleep(2_000); |
| bleScanner.stopScan(scanCallback); |
| |
| // Restore adapter state at end of test |
| if (bluetoothEnabledByTest) { |
| bluetoothAdapter.disable(); |
| } |
| } |
| |
| private static void doScheduleJob(Context ctx, String requestCode) { |
| final ComponentName JOB_COMPONENT_NAME = |
| new ComponentName("com.android.server.cts.device.batterystats", |
| SimpleJobService.class.getName()); |
| JobScheduler js = ctx.getSystemService(JobScheduler.class); |
| if (js == null) { |
| Log.e(TAG, "JobScheduler service not available"); |
| tellHostActionFinished(ACTION_JOB_SCHEDULE, requestCode); |
| return; |
| } |
| final JobInfo job = (new JobInfo.Builder(1, JOB_COMPONENT_NAME)) |
| .setOverrideDeadline(0) |
| .build(); |
| CountDownLatch latch = SimpleJobService.resetCountDownLatch(); |
| js.schedule(job); |
| // Job starts in main thread so wait in another thread to see if job finishes. |
| new AsyncTask<Void, Void, Void>() { |
| @Override |
| protected Void doInBackground(Void... params) { |
| waitForReceiver(null, 60_000, latch, null); |
| tellHostActionFinished(ACTION_JOB_SCHEDULE, requestCode); |
| return null; |
| } |
| }.execute(); |
| } |
| |
| private static void doNothingAsync(Context ctx, String requestCode, String actionCode) { |
| new AsyncTask<Void, Void, Void>() { |
| @Override |
| protected Void doInBackground(Void... params) { |
| sleep(DO_NOTHING_TIMEOUT); |
| return null; |
| } |
| |
| @Override |
| protected void onPostExecute(Void nothing) { |
| if (ctx instanceof Activity) { |
| ((Activity) ctx).finish(); |
| tellHostActionFinished(actionCode, requestCode); |
| } |
| } |
| }.execute(); |
| } |
| |
| private static void doSync(Context ctx, String requestCode) { |
| BatteryStatsAuthenticator.removeAllAccounts(ctx); |
| final Account account = BatteryStatsAuthenticator.getTestAccount(); |
| // Create the test account. |
| BatteryStatsAuthenticator.ensureTestAccount(ctx); |
| // Force set is syncable. |
| ContentResolver.setMasterSyncAutomatically(true); |
| ContentResolver.setIsSyncable(account, BatteryStatsProvider.AUTHORITY, 1); |
| |
| new AsyncTask<Void, Void, Void>() { |
| @Override |
| protected Void doInBackground(Void... params) { |
| try { |
| Log.v(TAG, "Starting sync"); |
| BatteryStatsSyncAdapter.requestSync(account); |
| sleep(500); |
| } catch (Exception e) { |
| Log.e(TAG, "Exception trying to sync", e); |
| } |
| BatteryStatsAuthenticator.removeAllAccounts(ctx); |
| return null; |
| } |
| |
| @Override |
| protected void onPostExecute(Void aVoid) { |
| super.onPostExecute(aVoid); |
| Log.v(TAG, "Finished sync method"); |
| // If ctx is an Activity, finish it when sync is done. If it's a service, don't. |
| if (ctx instanceof Activity) { |
| ((Activity) ctx).finish(); |
| } |
| tellHostActionFinished(ACTION_SYNC, requestCode); |
| } |
| }.execute(); |
| } |
| |
| /** Register receiver to determine when given action is complete. */ |
| private static BroadcastReceiver registerReceiver( |
| Context ctx, CountDownLatch onReceiveLatch, IntentFilter intentFilter) { |
| BroadcastReceiver receiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| onReceiveLatch.countDown(); |
| } |
| }; |
| // run Broadcast receiver in a different thread since the foreground activity will wait. |
| HandlerThread handlerThread = new HandlerThread("br_handler_thread"); |
| handlerThread.start(); |
| Looper looper = handlerThread.getLooper(); |
| Handler handler = new Handler(looper); |
| ctx.registerReceiver(receiver, intentFilter, null, handler); |
| return receiver; |
| } |
| |
| /** |
| * Uses the receiver to wait until the action is complete. ctx and receiver may be null if no |
| * receiver is needed to be unregistered. |
| */ |
| private static void waitForReceiver(Context ctx, |
| int maxWaitTimeMs, CountDownLatch latch, BroadcastReceiver receiver) { |
| try { |
| boolean didFinish = latch.await(maxWaitTimeMs, TimeUnit.MILLISECONDS); |
| if (didFinish) { |
| Log.v(TAG, "Finished performing action"); |
| } else { |
| // This is not necessarily a problem. If we just want to make sure a count was |
| // recorded for the request, it doesn't matter if the action actually finished. |
| Log.w(TAG, "Did not finish in specified time."); |
| } |
| } catch (InterruptedException e) { |
| Log.e(TAG, "Interrupted exception while awaiting action to finish", e); |
| } |
| if (ctx != null && receiver != null) { |
| ctx.unregisterReceiver(receiver); |
| } |
| } |
| |
| /** Communicates to hostside (via logcat) that action has completed (regardless of success). */ |
| private static void tellHostActionFinished(String actionCode, String requestCode) { |
| String s = String.format("Completed performing %s for request %s", actionCode, requestCode); |
| Log.i(TAG, s); |
| } |
| |
| /** Determines whether the package is running as a background process. */ |
| private static boolean isAppInBackground(Context context) throws ReflectiveOperationException { |
| String pkgName = context.getPackageName(); |
| ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); |
| List<ActivityManager.RunningAppProcessInfo> processes = am.getRunningAppProcesses(); |
| if (processes == null) { |
| return false; |
| } |
| for (ActivityManager.RunningAppProcessInfo r : processes) { |
| // BatteryStatsImpl treats as background if procState is >= |
| // Activitymanager.PROCESS_STATE_IMPORTANT_BACKGROUND (corresponding |
| // to BatteryStats.PROCESS_STATE_BACKGROUND). |
| // Due to lack of permissions, only the current app should show up in the list of |
| // processes, which is desired in this case; but in case this changes later, we check |
| // that the package name matches anyway. |
| int processState = -1; |
| int backgroundCode = -1; |
| try { |
| processState = ActivityManager.RunningAppProcessInfo.class |
| .getField("processState").getInt(r); |
| backgroundCode = (Integer) ActivityManager.class |
| .getDeclaredField("PROCESS_STATE_IMPORTANT_BACKGROUND").get(null); |
| } catch (ReflectiveOperationException ex) { |
| Log.e(TAG, "Failed to get proc state info via reflection", ex); |
| throw ex; |
| } |
| if (processState < backgroundCode) { // if foreground process |
| for (String rpkg : r.pkgList) { |
| if (pkgName.equals(rpkg)) { |
| return false; |
| } |
| } |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Makes sure app is in desired state, either background (if shouldBeBg = true) or foreground |
| * (if shouldBeBg = false). |
| * Tries for up to PROC_STATE_CHECK_ATTEMPTS seconds. If app is still not in the correct state, |
| * throws an AssertionError failure to crash the app. |
| */ |
| public static void checkAppState( |
| Context context, boolean shouldBeBg, String actionCode, String requestCode) { |
| final String errMsg = "App is " + (shouldBeBg ? "not " : "") + "a background process!"; |
| try { |
| for (int attempt = 0; attempt < PROC_STATE_CHECK_ATTEMPTS; attempt++) { |
| if (shouldBeBg == isAppInBackground(context)) { |
| return; // No problems. |
| } else { |
| if (attempt < PROC_STATE_CHECK_ATTEMPTS - 1) { |
| Log.w(TAG, errMsg + " Trying again in 1s."); |
| sleep(1_000); |
| } else { |
| Log.e(TAG, errMsg + " Quiting app."); |
| BatteryStatsBgVsFgActions.tellHostActionFinished(actionCode, requestCode); |
| Assert.fail(errMsg + " Test requires app to be in the correct state."); |
| } |
| } |
| } |
| } catch(ReflectiveOperationException ex) { |
| Log.w(TAG, "Couldn't determine if app is in background. Proceeding with test anyway."); |
| } |
| } |
| |
| /** Puts the current thread to sleep. */ |
| private static void sleep(int millis) { |
| try { |
| Thread.sleep(millis); |
| } catch (InterruptedException e) { |
| Log.e(TAG, "Interrupted exception while sleeping", e); |
| } |
| } |
| } |