blob: 8d8adb1901386450ef9d1b69f59d75840ca03f8b [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.alarmmanager.cts;
import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import android.alarmmanager.alarmtestapp.cts.TestAlarmReceiver;
import android.alarmmanager.alarmtestapp.cts.TestAlarmScheduler;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.SystemClock;
import android.support.test.uiautomator.UiDevice;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.AppStandbyUtils;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Tests that app standby imposes the appropriate restrictions on alarms
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class AppStandbyTests {
private static final String TAG = AppStandbyTests.class.getSimpleName();
private static final String TEST_APP_PACKAGE = "android.alarmmanager.alarmtestapp.cts";
private static final String TEST_APP_RECEIVER = TEST_APP_PACKAGE + ".TestAlarmScheduler";
private static final long DEFAULT_WAIT = 4_000;
private static final long POLL_INTERVAL = 200;
// Tweaked alarm manager constants to facilitate testing
private static final long ALLOW_WHILE_IDLE_SHORT_TIME = 15_000;
private static final long MIN_FUTURITY = 2_000;
private static final long[] APP_STANDBY_DELAYS = {0, 10_000, 20_000, 30_000, 600_000};
private static final String[] APP_BUCKET_TAGS = {
"active",
"working_set",
"frequent",
"rare",
"never"
};
private static final String[] APP_BUCKET_KEYS = {
"standby_active_delay",
"standby_working_delay",
"standby_frequent_delay",
"standby_rare_delay",
"standby_never_delay",
};
// Save the state before running tests to restore it after we finish testing.
private static boolean sOrigAppStandbyEnabled;
private Context mContext;
private ComponentName mAlarmScheduler;
private UiDevice mUiDevice;
private AtomicInteger mAlarmCount;
private volatile long mLastAlarmTime;
private final BroadcastReceiver mAlarmStateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
mAlarmCount.getAndAdd(intent.getIntExtra(TestAlarmReceiver.EXTRA_ALARM_COUNT, 1));
mLastAlarmTime = SystemClock.elapsedRealtime();
Log.d(TAG, "No. of expirations: " + mAlarmCount
+ " elapsed: " + SystemClock.elapsedRealtime());
}
};
@BeforeClass
public static void setUpTests() throws Exception {
sOrigAppStandbyEnabled = AppStandbyUtils.isAppStandbyEnabledAtRuntime();
if (!sOrigAppStandbyEnabled) {
AppStandbyUtils.setAppStandbyEnabledAtRuntime(true);
// Give system sometime to initialize itself.
Thread.sleep(100);
}
}
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getTargetContext();
mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
mAlarmScheduler = new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER);
mAlarmCount = new AtomicInteger(0);
updateAlarmManagerConstants();
setBatteryCharging(false);
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(TestAlarmReceiver.ACTION_REPORT_ALARM_EXPIRED);
mContext.registerReceiver(mAlarmStateReceiver, intentFilter);
assumeTrue("App Standby not enabled on device", AppStandbyUtils.isAppStandbyEnabled());
setAppStandbyBucket("active");
scheduleAlarm(SystemClock.elapsedRealtime(), false, 0);
Thread.sleep(MIN_FUTURITY);
assertTrue("Alarm not sent when app in active", waitForAlarms(1));
}
private void scheduleAlarm(long triggerMillis, boolean allowWhileIdle, long interval) {
final Intent setAlarmIntent = new Intent(TestAlarmScheduler.ACTION_SET_ALARM);
setAlarmIntent.setComponent(mAlarmScheduler);
setAlarmIntent.putExtra(TestAlarmScheduler.EXTRA_TYPE, ELAPSED_REALTIME_WAKEUP);
setAlarmIntent.putExtra(TestAlarmScheduler.EXTRA_TRIGGER_TIME, triggerMillis);
setAlarmIntent.putExtra(TestAlarmScheduler.EXTRA_REPEAT_INTERVAL, interval);
setAlarmIntent.putExtra(TestAlarmScheduler.EXTRA_ALLOW_WHILE_IDLE, allowWhileIdle);
setAlarmIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
mContext.sendBroadcast(setAlarmIntent);
}
private void testBucketDelay(int bucketIndex) throws Exception {
setAppStandbyBucket(APP_BUCKET_TAGS[bucketIndex]);
final long triggerTime = SystemClock.elapsedRealtime() + MIN_FUTURITY;
final long minTriggerTime = mLastAlarmTime + APP_STANDBY_DELAYS[bucketIndex];
scheduleAlarm(triggerTime, false, 0);
Thread.sleep(MIN_FUTURITY);
if (triggerTime + DEFAULT_WAIT < minTriggerTime) {
assertFalse("Alarm went off before " + APP_BUCKET_TAGS[bucketIndex] + " delay",
waitForAlarms(1));
Thread.sleep(minTriggerTime - SystemClock.elapsedRealtime());
}
assertTrue("Deferred alarm did not go off after " + APP_BUCKET_TAGS[bucketIndex] + " delay",
waitForAlarms(1));
}
@Test
public void testWorkingSetDelay() throws Exception {
testBucketDelay(1);
}
@Test
public void testFrequentDelay() throws Exception {
testBucketDelay(2);
}
@Test
public void testRareDelay() throws Exception {
testBucketDelay(3);
}
@Test
public void testBucketUpgradeToSmallerDelay() throws Exception {
setAppStandbyBucket(APP_BUCKET_TAGS[2]);
final long triggerTime = SystemClock.elapsedRealtime() + MIN_FUTURITY;
final long workingSetExpectedTrigger = mLastAlarmTime + APP_STANDBY_DELAYS[1];
scheduleAlarm(triggerTime, false, 0);
Thread.sleep(workingSetExpectedTrigger - SystemClock.elapsedRealtime());
assertFalse("The alarm went off before frequent delay", waitForAlarms(1));
setAppStandbyBucket(APP_BUCKET_TAGS[1]);
assertTrue("The alarm did not go off when app bucket upgraded to working_set",
waitForAlarms(1));
}
/**
* This is different to {@link #testBucketUpgradeToSmallerDelay()} in the sense that the bucket
* upgrade shifts eligibility to a point earlier than when the alarm is scheduled for.
* The alarm must then go off as soon as possible - at either the scheduled time or the bucket
* change, whichever happened later.
*/
@Test
public void testBucketUpgradeToNoDelay() throws Exception {
setAppStandbyBucket(APP_BUCKET_TAGS[3]);
final long triggerTime1 = mLastAlarmTime + APP_STANDBY_DELAYS[2];
scheduleAlarm(triggerTime1, false, 0);
Thread.sleep(triggerTime1 - SystemClock.elapsedRealtime());
assertFalse("The alarm went off after frequent delay when app in rare bucket",
waitForAlarms(1));
setAppStandbyBucket(APP_BUCKET_TAGS[1]);
assertTrue("The alarm did not go off when app bucket upgraded to working_set",
waitForAlarms(1));
// Once more
setAppStandbyBucket(APP_BUCKET_TAGS[3]);
final long triggerTime2 = mLastAlarmTime + APP_STANDBY_DELAYS[2];
scheduleAlarm(triggerTime2, false, 0);
setAppStandbyBucket(APP_BUCKET_TAGS[0]);
Thread.sleep(triggerTime2 - SystemClock.elapsedRealtime());
assertTrue("The alarm did not go off as scheduled when the app was in active",
waitForAlarms(1));
}
@Test
public void testAllowWhileIdleAlarms() throws Exception {
final long firstTrigger = SystemClock.elapsedRealtime() + MIN_FUTURITY;
scheduleAlarm(firstTrigger, true, 0);
Thread.sleep(MIN_FUTURITY);
assertTrue("first allow_while_idle alarm did not go off as scheduled", waitForAlarms(1));
scheduleAlarm(mLastAlarmTime + 9_000, true, 0);
// First check for the case where allow_while_idle delay should supersede app standby
setAppStandbyBucket(APP_BUCKET_TAGS[1]);
Thread.sleep(APP_STANDBY_DELAYS[1]);
assertFalse("allow_while_idle alarm went off before short time", waitForAlarms(1));
long expectedTriggerTime = mLastAlarmTime + ALLOW_WHILE_IDLE_SHORT_TIME;
Thread.sleep(expectedTriggerTime - SystemClock.elapsedRealtime());
assertTrue("allow_while_idle alarm did not go off after short time", waitForAlarms(1));
// Now the other case, app standby delay supersedes the allow_while_idle delay
scheduleAlarm(mLastAlarmTime + 12_000, true, 0);
setAppStandbyBucket(APP_BUCKET_TAGS[2]);
Thread.sleep(ALLOW_WHILE_IDLE_SHORT_TIME);
assertFalse("allow_while_idle alarm went off before " + APP_STANDBY_DELAYS[2]
+ "ms, when in bucket " + APP_BUCKET_TAGS[2], waitForAlarms(1));
expectedTriggerTime = mLastAlarmTime + APP_STANDBY_DELAYS[2];
Thread.sleep(expectedTriggerTime - SystemClock.elapsedRealtime());
assertTrue("allow_while_idle alarm did not go off even after " + APP_STANDBY_DELAYS[2]
+ "ms, when in bucket " + APP_BUCKET_TAGS[2], waitForAlarms(1));
}
@Test
public void testPowerWhitelistedAlarmNotBlocked() throws Exception {
setAppStandbyBucket(APP_BUCKET_TAGS[3]);
setPowerWhitelisted(true);
final long triggerTime = SystemClock.elapsedRealtime() + MIN_FUTURITY;
scheduleAlarm(triggerTime, false, 0);
Thread.sleep(MIN_FUTURITY);
assertTrue("Alarm did not go off for whitelisted app in rare bucket", waitForAlarms(1));
setPowerWhitelisted(false);
}
@After
public void tearDown() throws Exception {
setPowerWhitelisted(false);
setBatteryCharging(true);
deleteAlarmManagerConstants();
final Intent cancelAlarmsIntent = new Intent(TestAlarmScheduler.ACTION_CANCEL_ALL_ALARMS);
cancelAlarmsIntent.setComponent(mAlarmScheduler);
mContext.sendBroadcast(cancelAlarmsIntent);
mContext.unregisterReceiver(mAlarmStateReceiver);
// Broadcast unregister may race with the next register in setUp
Thread.sleep(500);
}
@AfterClass
public static void tearDownTests() throws Exception {
if (!sOrigAppStandbyEnabled) {
AppStandbyUtils.setAppStandbyEnabledAtRuntime(sOrigAppStandbyEnabled);
}
}
private void updateAlarmManagerConstants() throws IOException {
final StringBuffer cmd = new StringBuffer("settings put global alarm_manager_constants ");
cmd.append("min_futurity="); cmd.append(MIN_FUTURITY);
cmd.append(",allow_while_idle_short_time="); cmd.append(ALLOW_WHILE_IDLE_SHORT_TIME);
for (int i = 0; i < APP_STANDBY_DELAYS.length; i++) {
cmd.append(",");
cmd.append(APP_BUCKET_KEYS[i]); cmd.append("="); cmd.append(APP_STANDBY_DELAYS[i]);
}
executeAndLog(cmd.toString());
}
private void setPowerWhitelisted(boolean whitelist) throws IOException {
final StringBuffer cmd = new StringBuffer("cmd deviceidle whitelist ");
cmd.append(whitelist ? "+" : "-");
cmd.append(TEST_APP_PACKAGE);
executeAndLog(cmd.toString());
}
private void deleteAlarmManagerConstants() throws IOException {
executeAndLog("settings delete global alarm_manager_constants");
}
private void setAppStandbyBucket(String bucket) throws IOException {
executeAndLog("am set-standby-bucket " + TEST_APP_PACKAGE + " " + bucket);
}
private void setBatteryCharging(final boolean charging) throws Exception {
final BatteryManager bm = mContext.getSystemService(BatteryManager.class);
final String cmd = "dumpsys battery " + (charging ? "reset" : "unplug");
executeAndLog(cmd);
if (!charging) {
assertTrue("Battery could not be unplugged", waitUntil(() -> !bm.isCharging(), 5_000));
}
}
private String executeAndLog(String cmd) throws IOException {
final String output = mUiDevice.executeShellCommand(cmd).trim();
Log.d(TAG, "command: [" + cmd + "], output: [" + output + "]");
return output;
}
private boolean waitForAlarms(final int numAlarms) throws InterruptedException {
final boolean success = waitUntil(() -> (mAlarmCount.get() == numAlarms), DEFAULT_WAIT);
mAlarmCount.set(0);
return success;
}
private boolean waitUntil(Condition condition, long timeout) throws InterruptedException {
final long deadLine = SystemClock.uptimeMillis() + timeout;
while (!condition.isMet() && SystemClock.uptimeMillis() < deadLine) {
Thread.sleep(POLL_INTERVAL);
}
return condition.isMet();
}
@FunctionalInterface
interface Condition {
boolean isMet();
}
}