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 android.alarmmanager.cts;
18 
19 import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
20 
21 import static org.junit.Assert.assertFalse;
22 import static org.junit.Assert.assertTrue;
23 import static org.junit.Assume.assumeTrue;
24 
25 import android.alarmmanager.alarmtestapp.cts.TestAlarmReceiver;
26 import android.alarmmanager.alarmtestapp.cts.TestAlarmScheduler;
27 import android.alarmmanager.util.AlarmManagerDeviceConfigHelper;
28 import android.app.Activity;
29 import android.content.BroadcastReceiver;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.IntentFilter;
34 import android.os.BatteryManager;
35 import android.os.SystemClock;
36 import android.platform.test.annotations.AppModeFull;
37 import android.support.test.uiautomator.UiDevice;
38 import android.util.Log;
39 import android.util.LongArray;
40 
41 import androidx.test.InstrumentationRegistry;
42 import androidx.test.filters.LargeTest;
43 import androidx.test.runner.AndroidJUnit4;
44 
45 import com.android.compatibility.common.util.AppStandbyUtils;
46 
47 import org.junit.After;
48 import org.junit.AfterClass;
49 import org.junit.Before;
50 import org.junit.BeforeClass;
51 import org.junit.Test;
52 import org.junit.runner.RunWith;
53 
54 import java.io.IOException;
55 import java.util.concurrent.CountDownLatch;
56 import java.util.concurrent.TimeUnit;
57 import java.util.concurrent.atomic.AtomicInteger;
58 import java.util.function.BooleanSupplier;
59 
60 /**
61  * Tests that app standby imposes the appropriate restrictions on alarms
62  */
63 @AppModeFull
64 @LargeTest
65 @RunWith(AndroidJUnit4.class)
66 public class AppStandbyTests {
67     private static final String TAG = AppStandbyTests.class.getSimpleName();
68     static final String TEST_APP_PACKAGE = "android.alarmmanager.alarmtestapp.cts";
69     private static final String TEST_APP_RECEIVER = TEST_APP_PACKAGE + ".TestAlarmScheduler";
70 
71     private static final long DEFAULT_WAIT = 2_000;
72     private static final long POLL_INTERVAL = 200;
73 
74     // Tweaked alarm manager constants to facilitate testing
75     private static final long MIN_FUTURITY = 1_000;
76 
77     // Not touching ACTIVE and RARE parameters for this test
78     private static final int WORKING_INDEX = 0;
79     private static final int FREQUENT_INDEX = 1;
80     private static final int RARE_INDEX = 2;
81     private static final String[] APP_BUCKET_TAGS = {
82             "working_set",
83             "frequent",
84             "rare",
85     };
86 
87     private static final long APP_STANDBY_WINDOW = 10_000;
88     private static final long MIN_WINDOW = 100;
89     private static final String[] APP_BUCKET_QUOTA_KEYS = {
90             "standby_quota_working",
91             "standby_quota_frequent",
92             "standby_quota_rare",
93     };
94     private static final int[] APP_STANDBY_QUOTAS = {
95             5,  // Working set
96             3,  // Frequent
97             1,  // Rare
98     };
99 
100     // Save the state before running tests to restore it after we finish testing.
101     private static boolean sOrigAppStandbyEnabled;
102     // Test app's alarm history to help predict when a subsequent alarm is going to get deferred.
103     private static TestAlarmHistory sAlarmHistory;
104     private static Context sContext = InstrumentationRegistry.getTargetContext();
105     private static UiDevice sUiDevice = UiDevice.getInstance(
106             InstrumentationRegistry.getInstrumentation());
107 
108     private ComponentName mAlarmScheduler;
109     private AtomicInteger mAlarmCount;
110     private AlarmManagerDeviceConfigHelper mConfigHelper = new AlarmManagerDeviceConfigHelper();
111 
112     private final BroadcastReceiver mAlarmStateReceiver = new BroadcastReceiver() {
113         @Override
114         public void onReceive(Context context, Intent intent) {
115             mAlarmCount.getAndAdd(intent.getIntExtra(TestAlarmReceiver.EXTRA_ALARM_COUNT, 1));
116             final long nowElapsed = SystemClock.elapsedRealtime();
117             sAlarmHistory.addTime(nowElapsed);
118             Log.d(TAG, "No. of expirations: " + mAlarmCount + " elapsed: " + nowElapsed);
119         }
120     };
121 
122     @BeforeClass
setUpTests()123     public static void setUpTests() throws Exception {
124         sAlarmHistory = new TestAlarmHistory();
125         sOrigAppStandbyEnabled = AppStandbyUtils.isAppStandbyEnabledAtRuntime();
126         if (!sOrigAppStandbyEnabled) {
127             AppStandbyUtils.setAppStandbyEnabledAtRuntime(true);
128 
129             // Give system sometime to initialize itself.
130             Thread.sleep(100);
131         }
132     }
133 
134     @Before
setUp()135     public void setUp() throws Exception {
136         mAlarmScheduler = new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER);
137         mAlarmCount = new AtomicInteger(0);
138         updateAlarmManagerConstants();
139         setBatteryCharging(false);
140         final IntentFilter intentFilter = new IntentFilter();
141         intentFilter.addAction(TestAlarmReceiver.ACTION_REPORT_ALARM_EXPIRED);
142         sContext.registerReceiver(mAlarmStateReceiver, intentFilter);
143         assumeTrue("App Standby not enabled on device", AppStandbyUtils.isAppStandbyEnabled());
144     }
145 
scheduleAlarm(long triggerMillis, long interval)146     private void scheduleAlarm(long triggerMillis, long interval) throws InterruptedException {
147         final Intent setAlarmIntent = new Intent(TestAlarmScheduler.ACTION_SET_ALARM);
148         setAlarmIntent.setComponent(mAlarmScheduler);
149         setAlarmIntent.putExtra(TestAlarmScheduler.EXTRA_TYPE, ELAPSED_REALTIME_WAKEUP);
150         setAlarmIntent.putExtra(TestAlarmScheduler.EXTRA_TRIGGER_TIME, triggerMillis);
151         setAlarmIntent.putExtra(TestAlarmScheduler.EXTRA_WINDOW_LENGTH, MIN_WINDOW);
152         setAlarmIntent.putExtra(TestAlarmScheduler.EXTRA_REPEAT_INTERVAL, interval);
153         setAlarmIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
154         final CountDownLatch resultLatch = new CountDownLatch(1);
155         sContext.sendOrderedBroadcast(setAlarmIntent, null, new BroadcastReceiver() {
156             @Override
157             public void onReceive(Context context, Intent intent) {
158                 resultLatch.countDown();
159             }
160         }, null, Activity.RESULT_CANCELED, null, null);
161         assertTrue("Request did not complete", resultLatch.await(10, TimeUnit.SECONDS));
162     }
163 
testSimpleQuotaDeferral(int bucketIndex)164     public void testSimpleQuotaDeferral(int bucketIndex) throws Exception {
165         setTestAppStandbyBucket(APP_BUCKET_TAGS[bucketIndex]);
166         final int quota = APP_STANDBY_QUOTAS[bucketIndex];
167 
168         long startElapsed = SystemClock.elapsedRealtime();
169         final long freshWindowPoint = sAlarmHistory.getLast(1) + APP_STANDBY_WINDOW;
170         if (freshWindowPoint > startElapsed) {
171             Thread.sleep(freshWindowPoint - startElapsed);
172             startElapsed = freshWindowPoint;
173             // Now we should have no alarms in the past APP_STANDBY_WINDOW
174         }
175         final long desiredTrigger = startElapsed + APP_STANDBY_WINDOW;
176         final long firstTrigger = startElapsed + 4_000;
177         assertTrue("Quota too large for test",
178                 firstTrigger + ((quota - 1) * MIN_FUTURITY) < desiredTrigger);
179         for (int i = 0; i < quota; i++) {
180             final long trigger = firstTrigger + (i * MIN_FUTURITY);
181             scheduleAlarm(trigger, 0);
182             Thread.sleep(trigger - SystemClock.elapsedRealtime());
183             assertTrue("Alarm within quota not firing as expected", waitForAlarm());
184         }
185 
186         // Now quota is reached, any subsequent alarm should get deferred.
187         scheduleAlarm(desiredTrigger, 0);
188         Thread.sleep(desiredTrigger - SystemClock.elapsedRealtime());
189         assertFalse("Alarm exceeding quota not deferred", waitForAlarm());
190         final long minTrigger = firstTrigger + APP_STANDBY_WINDOW;
191         Thread.sleep(minTrigger - SystemClock.elapsedRealtime());
192         assertTrue("Alarm exceeding quota not delivered after expected delay", waitForAlarm());
193     }
194 
195     @Test
testActiveQuota()196     public void testActiveQuota() throws Exception {
197         setTestAppStandbyBucket("active");
198         long nextTrigger = SystemClock.elapsedRealtime() + MIN_FUTURITY;
199         for (int i = 0; i < 3; i++) {
200             scheduleAlarm(nextTrigger, 0);
201             Thread.sleep(MIN_FUTURITY);
202             assertTrue("Alarm not received as expected when app is in active", waitForAlarm());
203             nextTrigger += MIN_FUTURITY;
204         }
205     }
206 
207     @Test
testWorkingQuota()208     public void testWorkingQuota() throws Exception {
209         testSimpleQuotaDeferral(WORKING_INDEX);
210     }
211 
212     @Test
testFrequentQuota()213     public void testFrequentQuota() throws Exception {
214         testSimpleQuotaDeferral(FREQUENT_INDEX);
215     }
216 
217     @Test
testRareQuota()218     public void testRareQuota() throws Exception {
219         testSimpleQuotaDeferral(RARE_INDEX);
220     }
221 
222     @Test
testNeverQuota()223     public void testNeverQuota() throws Exception {
224         setTestAppStandbyBucket("never");
225         final long expectedTrigger = SystemClock.elapsedRealtime() + MIN_FUTURITY;
226         scheduleAlarm(expectedTrigger, 0);
227         Thread.sleep(10_000);
228         assertFalse("Alarm received when app was in never bucket", waitForAlarm());
229     }
230 
231     @Test
testPowerWhitelistedAlarmNotBlocked()232     public void testPowerWhitelistedAlarmNotBlocked() throws Exception {
233         setTestAppStandbyBucket(APP_BUCKET_TAGS[RARE_INDEX]);
234         setPowerWhitelisted(true);
235         final long triggerTime = SystemClock.elapsedRealtime() + MIN_FUTURITY;
236         scheduleAlarm(triggerTime, 0);
237         Thread.sleep(MIN_FUTURITY);
238         assertTrue("Alarm did not go off for whitelisted app in rare bucket", waitForAlarm());
239         setPowerWhitelisted(false);
240     }
241 
242     @After
tearDown()243     public void tearDown() throws Exception {
244         setPowerWhitelisted(false);
245         setBatteryCharging(true);
246         mConfigHelper.restoreAll();
247         final Intent cancelAlarmsIntent = new Intent(TestAlarmScheduler.ACTION_CANCEL_ALL_ALARMS);
248         cancelAlarmsIntent.setComponent(mAlarmScheduler);
249         sContext.sendBroadcast(cancelAlarmsIntent);
250         sContext.unregisterReceiver(mAlarmStateReceiver);
251         // Broadcast unregister may race with the next register in setUp
252         Thread.sleep(500);
253     }
254 
255     @AfterClass
tearDownTests()256     public static void tearDownTests() throws Exception {
257         if (!sOrigAppStandbyEnabled) {
258             AppStandbyUtils.setAppStandbyEnabledAtRuntime(sOrigAppStandbyEnabled);
259         }
260     }
261 
updateAlarmManagerConstants()262     private void updateAlarmManagerConstants() {
263         mConfigHelper.with("min_futurity", MIN_FUTURITY)
264                 .with("app_standby_window", APP_STANDBY_WINDOW)
265                 .with("min_window", MIN_WINDOW)
266                 .with("exact_alarm_deny_list", TEST_APP_PACKAGE);
267         for (int i = 0; i < APP_STANDBY_QUOTAS.length; i++) {
268             mConfigHelper.with(APP_BUCKET_QUOTA_KEYS[i], APP_STANDBY_QUOTAS[i]);
269         }
270         mConfigHelper.commitAndAwaitPropagation();
271     }
272 
setPowerWhitelisted(boolean whitelist)273     private void setPowerWhitelisted(boolean whitelist) throws IOException {
274         final StringBuffer cmd = new StringBuffer("cmd deviceidle whitelist ");
275         cmd.append(whitelist ? "+" : "-");
276         cmd.append(TEST_APP_PACKAGE);
277         executeAndLog(cmd.toString());
278     }
279 
setTestAppStandbyBucket(String bucket)280     static void setTestAppStandbyBucket(String bucket) throws IOException {
281         executeAndLog("am set-standby-bucket " + TEST_APP_PACKAGE + " " + bucket);
282     }
283 
setBatteryCharging(final boolean charging)284     private void setBatteryCharging(final boolean charging) throws Exception {
285         final BatteryManager bm = sContext.getSystemService(BatteryManager.class);
286         if (charging) {
287             executeAndLog("dumpsys battery reset");
288         } else {
289             executeAndLog("dumpsys battery unplug");
290             executeAndLog("dumpsys battery set status " +
291                     BatteryManager.BATTERY_STATUS_DISCHARGING);
292             assertTrue("Battery could not be unplugged", waitUntil(() -> !bm.isCharging(), 5_000));
293         }
294     }
295 
executeAndLog(String cmd)296     private static String executeAndLog(String cmd) throws IOException {
297         final String output = sUiDevice.executeShellCommand(cmd).trim();
298         Log.d(TAG, "command: [" + cmd + "], output: [" + output + "]");
299         return output;
300     }
301 
waitForAlarm()302     private boolean waitForAlarm() throws InterruptedException {
303         final boolean success = waitUntil(() -> (mAlarmCount.get() == 1), DEFAULT_WAIT);
304         mAlarmCount.set(0);
305         return success;
306     }
307 
waitUntil(BooleanSupplier condition, long timeout)308     private boolean waitUntil(BooleanSupplier condition, long timeout) throws InterruptedException {
309         final long deadLine = SystemClock.uptimeMillis() + timeout;
310         while (!condition.getAsBoolean() && SystemClock.uptimeMillis() < deadLine) {
311             Thread.sleep(POLL_INTERVAL);
312         }
313         return condition.getAsBoolean();
314     }
315 
316     private static final class TestAlarmHistory {
317         private LongArray mHistory = new LongArray();
318 
addTime(long timestamp)319         private synchronized void addTime(long timestamp) {
320             mHistory.add(timestamp);
321         }
322 
323         /**
324          * Get the xth alarm time from the end.
325          */
getLast(int x)326         private synchronized long getLast(int x) {
327             if (x == 0 || x > mHistory.size()) {
328                 return 0;
329             }
330             return mHistory.get(mHistory.size() - x);
331         }
332     }
333 }
334