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