blob: 58513afc126785716acd05460216d2a1dbdabd31 [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 android.app.AppOpsManager.MODE_IGNORED;
import static android.app.AppOpsManager.OPSTR_SCHEDULE_EXACT_ALARM;
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.alarmmanager.util.AlarmManagerDeviceConfigHelper;
import android.alarmmanager.util.Utils;
import android.app.Activity;
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.platform.test.annotations.AppModeFull;
import android.provider.DeviceConfig;
import android.util.Log;
import android.util.LongArray;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.AppOpsUtils;
import com.android.compatibility.common.util.AppStandbyUtils;
import com.android.compatibility.common.util.DeviceConfigStateHelper;
import com.android.compatibility.common.util.SystemUtil;
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.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BooleanSupplier;
/**
* Tests that app standby imposes the appropriate restrictions on alarms
*/
@AppModeFull
@LargeTest
@RunWith(AndroidJUnit4.class)
public class AppStandbyTests {
private static final String TAG = AppStandbyTests.class.getSimpleName();
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 = 2_000;
private static final long POLL_INTERVAL = 200;
// Tweaked alarm manager constants to facilitate testing
private static final long MIN_FUTURITY = 1_000;
// Not touching ACTIVE and RARE parameters for this test
private static final int WORKING_INDEX = 0;
private static final int FREQUENT_INDEX = 1;
private static final int RARE_INDEX = 2;
private static final String[] APP_BUCKET_TAGS = {
"working_set",
"frequent",
"rare",
};
private static final long APP_STANDBY_WINDOW = 10_000;
private static final long MIN_WINDOW = 100;
private static final String[] APP_BUCKET_QUOTA_KEYS = {
"standby_quota_working",
"standby_quota_frequent",
"standby_quota_rare",
};
private static final int[] APP_STANDBY_QUOTAS = {
5, // Working set
3, // Frequent
1, // Rare
};
// Save the state before running tests to restore it after we finish testing.
private static boolean sOrigAppStandbyEnabled;
// Test app's alarm history to help predict when a subsequent alarm is going to get deferred.
private static TestAlarmHistory sAlarmHistory;
// Make sure TARE isn't enabled for any of these tests.
private static final DeviceConfigStateHelper sTareDeviceConfigStateHelper =
new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_TARE);
private static Context sContext = InstrumentationRegistry.getTargetContext();
private ComponentName mAlarmScheduler;
private AtomicInteger mAlarmCount;
private AlarmManagerDeviceConfigHelper mConfigHelper = new AlarmManagerDeviceConfigHelper();
private final BroadcastReceiver mAlarmStateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
mAlarmCount.getAndAdd(intent.getIntExtra(TestAlarmReceiver.EXTRA_ALARM_COUNT, 1));
final long nowElapsed = SystemClock.elapsedRealtime();
sAlarmHistory.addTime(nowElapsed);
Log.d(TAG, "No. of expirations: " + mAlarmCount + " elapsed: " + nowElapsed);
}
};
@BeforeClass
public static void setUpTests() throws Exception {
sAlarmHistory = new TestAlarmHistory();
sOrigAppStandbyEnabled = AppStandbyUtils.isAppStandbyEnabledAtRuntime();
if (!sOrigAppStandbyEnabled) {
AppStandbyUtils.setAppStandbyEnabledAtRuntime(true);
// Give system sometime to initialize itself.
Thread.sleep(100);
}
// These tests are designed for the old quota system.
sTareDeviceConfigStateHelper.set("enable_tare_mode", "0");
}
@Before
public void setUp() throws Exception {
mAlarmScheduler = new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER);
mAlarmCount = new AtomicInteger(0);
// To make sure it doesn't get pinned to working_set on older versions.
AppOpsUtils.setUidMode(Utils.getPackageUid(TEST_APP_PACKAGE), OPSTR_SCHEDULE_EXACT_ALARM,
MODE_IGNORED);
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(TestAlarmReceiver.ACTION_REPORT_ALARM_EXPIRED);
sContext.registerReceiver(mAlarmStateReceiver, intentFilter,
Context.RECEIVER_EXPORTED_UNAUDITED);
setBatteryCharging(false);
updateAlarmManagerConstants();
assumeTrue("App Standby not enabled on device", AppStandbyUtils.isAppStandbyEnabled());
}
private void scheduleAlarm(long triggerMillis, long interval) throws InterruptedException {
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_WINDOW_LENGTH, MIN_WINDOW);
setAlarmIntent.putExtra(TestAlarmScheduler.EXTRA_REPEAT_INTERVAL, interval);
setAlarmIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
final CountDownLatch resultLatch = new CountDownLatch(1);
sContext.sendOrderedBroadcast(setAlarmIntent, null, new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
resultLatch.countDown();
}
}, null, Activity.RESULT_CANCELED, null, null);
assertTrue("Request did not complete", resultLatch.await(10, TimeUnit.SECONDS));
}
public void testSimpleQuotaDeferral(int bucketIndex) throws Exception {
setTestAppStandbyBucket(APP_BUCKET_TAGS[bucketIndex]);
final int quota = APP_STANDBY_QUOTAS[bucketIndex];
long startElapsed = SystemClock.elapsedRealtime();
final long freshWindowPoint = sAlarmHistory.getLast(1) + APP_STANDBY_WINDOW;
if (freshWindowPoint > startElapsed) {
Thread.sleep(freshWindowPoint - startElapsed);
startElapsed = freshWindowPoint;
// Now we should have no alarms in the past APP_STANDBY_WINDOW
}
final long desiredTrigger = startElapsed + APP_STANDBY_WINDOW;
final long firstTrigger = startElapsed + 4_000;
assertTrue("Quota too large for test",
firstTrigger + ((quota - 1) * MIN_FUTURITY) < desiredTrigger);
for (int i = 0; i < quota; i++) {
final long trigger = firstTrigger + (i * MIN_FUTURITY);
scheduleAlarm(trigger, 0);
Thread.sleep(trigger - SystemClock.elapsedRealtime());
assertTrue("Alarm within quota not firing as expected", waitForAlarm());
}
// Now quota is reached, any subsequent alarm should get deferred.
scheduleAlarm(desiredTrigger, 0);
Thread.sleep(desiredTrigger - SystemClock.elapsedRealtime());
assertFalse("Alarm exceeding quota not deferred", waitForAlarm());
final long minTrigger = firstTrigger + APP_STANDBY_WINDOW;
Thread.sleep(minTrigger - SystemClock.elapsedRealtime());
assertTrue("Alarm exceeding quota not delivered after expected delay", waitForAlarm());
}
@Test
public void testActiveQuota() throws Exception {
setTestAppStandbyBucket("active");
long nextTrigger = SystemClock.elapsedRealtime() + MIN_FUTURITY;
for (int i = 0; i < 3; i++) {
scheduleAlarm(nextTrigger, 0);
Thread.sleep(MIN_FUTURITY);
assertTrue("Alarm not received as expected when app is in active", waitForAlarm());
nextTrigger += MIN_FUTURITY;
}
}
@Test
public void testWorkingQuota() throws Exception {
testSimpleQuotaDeferral(WORKING_INDEX);
}
@Test
public void testFrequentQuota() throws Exception {
testSimpleQuotaDeferral(FREQUENT_INDEX);
}
@Test
public void testRareQuota() throws Exception {
testSimpleQuotaDeferral(RARE_INDEX);
}
@Test
public void testNeverQuota() throws Exception {
setTestAppStandbyBucket("never");
final long expectedTrigger = SystemClock.elapsedRealtime() + MIN_FUTURITY;
scheduleAlarm(expectedTrigger, 0);
Thread.sleep(10_000);
assertFalse("Alarm received when app was in never bucket", waitForAlarm());
}
@Test
public void testPowerWhitelistedAlarmNotBlocked() throws Exception {
setTestAppStandbyBucket(APP_BUCKET_TAGS[RARE_INDEX]);
setPowerAllowlisted(true);
final long triggerTime = SystemClock.elapsedRealtime() + MIN_FUTURITY;
scheduleAlarm(triggerTime, 0);
Thread.sleep(MIN_FUTURITY);
assertTrue("Alarm did not go off for whitelisted app in rare bucket", waitForAlarm());
setPowerAllowlisted(false);
}
@After
public void tearDown() throws Exception {
setPowerAllowlisted(false);
setBatteryCharging(true);
mConfigHelper.restoreAll();
final Intent cancelAlarmsIntent = new Intent(TestAlarmScheduler.ACTION_CANCEL_ALL_ALARMS);
cancelAlarmsIntent.setComponent(mAlarmScheduler);
sContext.sendBroadcast(cancelAlarmsIntent);
sContext.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);
}
sTareDeviceConfigStateHelper.restoreOriginalValues();
}
private void updateAlarmManagerConstants() {
mConfigHelper.with("min_futurity", MIN_FUTURITY)
.with("app_standby_window", APP_STANDBY_WINDOW)
.with("min_window", MIN_WINDOW);
for (int i = 0; i < APP_STANDBY_QUOTAS.length; i++) {
mConfigHelper.with(APP_BUCKET_QUOTA_KEYS[i], APP_STANDBY_QUOTAS[i]);
}
mConfigHelper.commitAndAwaitPropagation();
}
private void setPowerAllowlisted(boolean whitelist) throws IOException {
final StringBuffer cmd = new StringBuffer("cmd deviceidle whitelist ");
cmd.append(whitelist ? "+" : "-");
cmd.append(TEST_APP_PACKAGE);
executeAndLog(cmd.toString());
}
static void setTestAppStandbyBucket(String bucket) throws IOException {
executeAndLog("am set-standby-bucket " + TEST_APP_PACKAGE + " " + bucket);
}
private void setBatteryCharging(final boolean charging) throws Exception {
final BatteryManager bm = sContext.getSystemService(BatteryManager.class);
if (charging) {
executeAndLog("dumpsys battery reset");
} else {
executeAndLog("dumpsys battery unplug");
executeAndLog("dumpsys battery set status " +
BatteryManager.BATTERY_STATUS_DISCHARGING);
assertTrue("Battery could not be unplugged", waitUntil(() -> !bm.isCharging(), 5_000));
}
}
private static String executeAndLog(String cmd) throws IOException {
final String output = SystemUtil.runShellCommand(cmd).trim();
Log.d(TAG, "command: [" + cmd + "], output: [" + output + "]");
return output;
}
private boolean waitForAlarm() throws InterruptedException {
final boolean success = waitUntil(() -> (mAlarmCount.get() == 1), DEFAULT_WAIT);
mAlarmCount.set(0);
return success;
}
private boolean waitUntil(BooleanSupplier condition, long timeout) throws InterruptedException {
final long deadLine = SystemClock.uptimeMillis() + timeout;
while (!condition.getAsBoolean() && SystemClock.uptimeMillis() < deadLine) {
Thread.sleep(POLL_INTERVAL);
}
return condition.getAsBoolean();
}
private static final class TestAlarmHistory {
private LongArray mHistory = new LongArray();
private synchronized void addTime(long timestamp) {
mHistory.add(timestamp);
}
/**
* Get the xth alarm time from the end.
*/
private synchronized long getLast(int x) {
if (x == 0 || x > mHistory.size()) {
return 0;
}
return mHistory.get(mHistory.size() - x);
}
}
}