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