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