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