summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java68
-rw-r--r--apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java381
-rw-r--r--core/java/android/app/ActivityManagerInternal.java7
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java5
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java24
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java170
6 files changed, 652 insertions, 3 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index d0a1b027ec48..154b2d763af8 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -290,6 +290,8 @@ public class AlarmManagerService extends SystemService {
// List of alarms per uid deferred due to user applied background restrictions on the source app
SparseArray<ArrayList<Alarm>> mPendingBackgroundAlarms = new SparseArray<>();
+
+ private boolean mStartUserBeforeScheduledAlarms;
private long mNextWakeup;
private long mNextNonWakeup;
private long mNextWakeUpSetAt;
@@ -1382,6 +1384,7 @@ public class AlarmManagerService extends SystemService {
@GuardedBy("mLock")
AlarmStore mAlarmStore;
+ UserWakeupStore mUserWakeupStore;
// set to non-null if in idle mode; while in this mode, any alarms we don't want
// to run during this time are rescehduled to go off after this alarm.
Alarm mPendingIdleUntil = null;
@@ -1882,6 +1885,7 @@ public class AlarmManagerService extends SystemService {
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
mUseFrozenStateToDropListenerAlarms = Flags.useFrozenStateToDropListenerAlarms();
+ mStartUserBeforeScheduledAlarms = Flags.startUserBeforeScheduledAlarms();
if (mUseFrozenStateToDropListenerAlarms) {
final ActivityManager.UidFrozenStateChangedCallback callback = (uids, frozenStates) -> {
final int size = frozenStates.length;
@@ -2000,6 +2004,10 @@ public class AlarmManagerService extends SystemService {
Slog.w(TAG, "Failed to open alarm driver. Falling back to a handler.");
}
}
+ if (mStartUserBeforeScheduledAlarms) {
+ mUserWakeupStore = new UserWakeupStore();
+ mUserWakeupStore.init();
+ }
publishLocalService(AlarmManagerInternal.class, new LocalService());
publishBinderService(Context.ALARM_SERVICE, mService);
}
@@ -2041,6 +2049,9 @@ public class AlarmManagerService extends SystemService {
public void onUserStarting(TargetUser user) {
super.onUserStarting(user);
final int userId = user.getUserIdentifier();
+ if (mStartUserBeforeScheduledAlarms) {
+ mUserWakeupStore.onUserStarting(userId);
+ }
mHandler.post(() -> {
for (final int appId : mExactAlarmCandidates) {
final int uid = UserHandle.getUid(userId, appId);
@@ -3150,6 +3161,9 @@ public class AlarmManagerService extends SystemService {
pw.increaseIndent();
pw.print(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS,
mUseFrozenStateToDropListenerAlarms);
+ pw.println();
+ pw.print(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS,
+ mStartUserBeforeScheduledAlarms);
pw.decreaseIndent();
pw.println();
pw.println();
@@ -3398,6 +3412,12 @@ public class AlarmManagerService extends SystemService {
pw.println("]");
pw.println();
+ if (mStartUserBeforeScheduledAlarms) {
+ pw.println("Scheduled user wakeups:");
+ mUserWakeupStore.dump(pw, nowELAPSED);
+ pw.println();
+ }
+
pw.println("App Alarm history:");
mAppWakeupHistory.dump(pw, nowELAPSED);
@@ -3945,10 +3965,19 @@ public class AlarmManagerService extends SystemService {
formatNextAlarm(getContext(), alarmClock, userId));
}
mNextAlarmClockForUser.put(userId, alarmClock);
+ if (mStartUserBeforeScheduledAlarms) {
+ mUserWakeupStore.addUserWakeup(userId, convertToElapsed(
+ mNextAlarmClockForUser.get(userId).getTriggerTime(), RTC));
+ }
} else {
if (DEBUG_ALARM_CLOCK) {
Log.v(TAG, "Next AlarmClockInfoForUser(" + userId + "): None");
}
+ if (mStartUserBeforeScheduledAlarms) {
+ if (mActivityManagerInternal.isUserRunning(userId, 0)) {
+ mUserWakeupStore.removeUserWakeup(userId);
+ }
+ }
mNextAlarmClockForUser.remove(userId);
}
@@ -4003,13 +4032,20 @@ public class AlarmManagerService extends SystemService {
DateFormat.format(pattern, info.getTriggerTime()).toString();
}
+ @GuardedBy("mLock")
void rescheduleKernelAlarmsLocked() {
// Schedule the next upcoming wakeup alarm. If there is a deliverable batch
// prior to that which contains no wakeups, we schedule that as well.
final long nowElapsed = mInjector.getElapsedRealtimeMillis();
long nextNonWakeup = 0;
if (mAlarmStore.size() > 0) {
- final long firstWakeup = mAlarmStore.getNextWakeupDeliveryTime();
+ long firstWakeup = mAlarmStore.getNextWakeupDeliveryTime();
+ if (mStartUserBeforeScheduledAlarms) {
+ final long firstUserWakeup = mUserWakeupStore.getNextWakeupTime();
+ if (firstUserWakeup >= 0 && firstUserWakeup < firstWakeup) {
+ firstWakeup = firstUserWakeup;
+ }
+ }
final long first = mAlarmStore.getNextDeliveryTime();
if (firstWakeup != 0) {
mNextWakeup = firstWakeup;
@@ -4716,6 +4752,16 @@ public class AlarmManagerService extends SystemService {
+ ", elapsed=" + nowELAPSED);
}
+ if (mStartUserBeforeScheduledAlarms) {
+ final int[] userIds =
+ mUserWakeupStore.getUserIdsToWakeup(nowELAPSED);
+ for (int i = 0; i < userIds.length; i++) {
+ if (!mActivityManagerInternal.startUserInBackground(
+ userIds[i])) {
+ mUserWakeupStore.removeUserWakeup(userIds[i]);
+ }
+ }
+ }
mLastTrigger = nowELAPSED;
final int wakeUps = triggerAlarmsLocked(triggerList, nowELAPSED);
if (wakeUps == 0 && checkAllowNonWakeupDelayLocked(nowELAPSED)) {
@@ -5164,6 +5210,10 @@ public class AlarmManagerService extends SystemService {
IntentFilter sdFilter = new IntentFilter();
sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
sdFilter.addAction(Intent.ACTION_USER_STOPPED);
+ if (mStartUserBeforeScheduledAlarms) {
+ sdFilter.addAction(Intent.ACTION_LOCKED_BOOT_COMPLETED);
+ sdFilter.addAction(Intent.ACTION_USER_REMOVED);
+ }
sdFilter.addAction(Intent.ACTION_UID_REMOVED);
getContext().registerReceiverForAllUsers(this, sdFilter,
/* broadcastPermission */ null, /* scheduler */ null);
@@ -5194,6 +5244,22 @@ public class AlarmManagerService extends SystemService {
mTemporaryQuotaReserve.removeForUser(userHandle);
}
return;
+ case Intent.ACTION_LOCKED_BOOT_COMPLETED:
+ final int handle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ if (handle >= 0) {
+ if (mStartUserBeforeScheduledAlarms) {
+ mUserWakeupStore.onUserStarted(handle);
+ }
+ }
+ return;
+ case Intent.ACTION_USER_REMOVED:
+ final int user = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ if (user >= 0) {
+ if (mStartUserBeforeScheduledAlarms) {
+ mUserWakeupStore.onUserRemoved(user);
+ }
+ }
+ return;
case Intent.ACTION_UID_REMOVED:
mLastPriorityAlarmDispatch.delete(uid);
mRemovalHistory.delete(uid);
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
new file mode 100644
index 000000000000..a0d9133b93da
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.alarm;
+
+
+import android.annotation.Nullable;
+import android.os.Environment;
+import android.os.SystemClock;
+import android.util.AtomicFile;
+import android.util.IndentingPrintWriter;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.SparseLongArray;
+import android.util.TimeUtils;
+import android.util.Xml;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BackgroundThread;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * User wakeup store keeps the list of user ids with the times that user needs to be started in
+ * sorted list in order for alarms to execute even if user gets stopped.
+ * The list of user ids with at least one alarms scheduled is also persisted to the XML file to
+ * start them after the device reboot.
+ */
+public class UserWakeupStore {
+ private static final boolean DEBUG = false;
+
+ static final String USER_WAKEUP_TAG = UserWakeupStore.class.getSimpleName();
+ private static final String TAG_USERS = "users";
+ private static final String TAG_USER = "user";
+ private static final String ATTR_USER_ID = "user_id";
+ private static final String ATTR_VERSION = "version";
+
+ public static final int XML_VERSION_CURRENT = 1;
+ @VisibleForTesting
+ static final String ROOT_DIR_NAME = "alarms";
+ @VisibleForTesting
+ static final String USERS_FILE_NAME = "usersWithAlarmClocks.xml";
+
+ /**
+ * Time offset of user start before the original alarm time in milliseconds.
+ * Also used to schedule user start after reboot to avoid starting them simultaneously.
+ */
+ @VisibleForTesting
+ static final long BUFFER_TIME_MS = TimeUnit.SECONDS.toMillis(30);
+ /**
+ * Maximum time deviation limit to introduce a 5-second time window for user starts.
+ */
+ @VisibleForTesting
+ static final long USER_START_TIME_DEVIATION_LIMIT_MS = TimeUnit.SECONDS.toMillis(5);
+ /**
+ * Delay between two consecutive user starts scheduled during user wakeup store initialization.
+ */
+ @VisibleForTesting
+ static final long INITIAL_USER_START_SCHEDULING_DELAY_MS = TimeUnit.SECONDS.toMillis(5);
+
+ private final Object mUserWakeupLock = new Object();
+
+ /**
+ * A list of wakeups for users with scheduled alarms.
+ */
+ @GuardedBy("mUserWakeupLock")
+ private final SparseLongArray mUserStarts = new SparseLongArray();
+ /**
+ * A list of users that are in a phase after they have been started but before alarms were
+ * initialized.
+ */
+ @GuardedBy("mUserWakeupLock")
+ private final SparseLongArray mStartingUsers = new SparseLongArray();
+ private Executor mBackgroundExecutor;
+ private static final File USER_WAKEUP_DIR = new File(Environment.getDataSystemDirectory(),
+ ROOT_DIR_NAME);
+ private static final Random sRandom = new Random(500);
+
+ /**
+ * Initialize mUserWakeups with persisted values.
+ */
+ public void init() {
+ mBackgroundExecutor = BackgroundThread.getExecutor();
+ mBackgroundExecutor.execute(this::readUserIdList);
+ }
+
+ /**
+ * Add user wakeup for the alarm.
+ * @param userId Id of the user that scheduled alarm.
+ * @param alarmTime time when alarm is expected to trigger.
+ */
+ public void addUserWakeup(int userId, long alarmTime) {
+ synchronized (mUserWakeupLock) {
+ // This should not be needed, but if an app in the user is scheduling an alarm clock, we
+ // consider the user start complete. There is a dedicated removal when user is started.
+ mStartingUsers.delete(userId);
+ mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset());
+ }
+ updateUserListFile();
+ }
+
+ /**
+ * Remove wakeup scheduled for the user with given userId if present.
+ */
+ public void removeUserWakeup(int userId) {
+ synchronized (mUserWakeupLock) {
+ mUserStarts.delete(userId);
+ }
+ updateUserListFile();
+ }
+
+ /**
+ * Get ids of users that need to be started now.
+ * @param nowElapsed current time.
+ * @return user ids to be started, or empty if no user needs to be started.
+ */
+ public int[] getUserIdsToWakeup(long nowElapsed) {
+ synchronized (mUserWakeupLock) {
+ final int[] userIds = new int[mUserStarts.size()];
+ int index = 0;
+ for (int i = mUserStarts.size() - 1; i >= 0; i--) {
+ if (mUserStarts.valueAt(i) <= nowElapsed) {
+ userIds[index++] = mUserStarts.keyAt(i);
+ }
+ }
+ return Arrays.copyOfRange(userIds, 0, index);
+ }
+ }
+
+ /**
+ * Persist user ids that have alarms scheduled so that they can be started after device reboot.
+ */
+ private void updateUserListFile() {
+ mBackgroundExecutor.execute(() -> {
+ try {
+ writeUserIdList();
+ if (DEBUG) {
+ synchronized (mUserWakeupLock) {
+ Slog.i(USER_WAKEUP_TAG, "Printing out user wakeups " + mUserStarts.size());
+ for (int i = 0; i < mUserStarts.size(); i++) {
+ Slog.i(USER_WAKEUP_TAG, "User id: " + mUserStarts.keyAt(i) + " time: "
+ + mUserStarts.valueAt(i));
+ }
+ }
+ }
+ } catch (Exception e) {
+ Slog.e(USER_WAKEUP_TAG, "Failed to write " + e.getLocalizedMessage());
+ }
+ });
+ }
+
+ /**
+ * Return scheduled start time for user or -1 if user does not have alarm set.
+ */
+ @VisibleForTesting
+ long getWakeupTimeForUserForTest(int userId) {
+ synchronized (mUserWakeupLock) {
+ return mUserStarts.get(userId, -1);
+ }
+ }
+
+ /**
+ * Move user from wakeup list to starting user list.
+ */
+ public void onUserStarting(int userId) {
+ synchronized (mUserWakeupLock) {
+ mStartingUsers.put(userId, getWakeupTimeForUserForTest(userId));
+ mUserStarts.delete(userId);
+ }
+ }
+
+ /**
+ * Remove userId from starting user list once start is complete.
+ */
+ public void onUserStarted(int userId) {
+ synchronized (mUserWakeupLock) {
+ mStartingUsers.delete(userId);
+ }
+ updateUserListFile();
+ }
+
+ /**
+ * Remove userId from the store when the user is removed.
+ */
+ public void onUserRemoved(int userId) {
+ synchronized (mUserWakeupLock) {
+ mUserStarts.delete(userId);
+ mStartingUsers.delete(userId);
+ }
+ updateUserListFile();
+ }
+
+ /**
+ * Get the soonest wakeup time in the store.
+ */
+ public long getNextWakeupTime() {
+ long nextWakeupTime = -1;
+ synchronized (mUserWakeupLock) {
+ for (int i = 0; i < mUserStarts.size(); i++) {
+ if (mUserStarts.valueAt(i) < nextWakeupTime || nextWakeupTime == -1) {
+ nextWakeupTime = mUserStarts.valueAt(i);
+ }
+ }
+ }
+ return nextWakeupTime;
+ }
+
+ private static long getUserWakeupOffset() {
+ return sRandom.nextLong(USER_START_TIME_DEVIATION_LIMIT_MS * 2)
+ - USER_START_TIME_DEVIATION_LIMIT_MS;
+ }
+
+ /**
+ * Write a list of ids for users who have alarm scheduled.
+ * Sample XML file:
+ *
+ * <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
+ * <users version="1">
+ * <user user_id="12" />
+ * <user user_id="10" />
+ * </users>
+ * ~
+ */
+ private void writeUserIdList() {
+ final AtomicFile file = getUserWakeupFile();
+ if (file == null) {
+ return;
+ }
+ try (FileOutputStream fos = file.startWrite(SystemClock.uptimeMillis())) {
+ final XmlSerializer out = new FastXmlSerializer();
+ out.setOutput(fos, StandardCharsets.UTF_8.name());
+ out.startDocument(null, true);
+ out.startTag(null, TAG_USERS);
+ XmlUtils.writeIntAttribute(out, ATTR_VERSION, XML_VERSION_CURRENT);
+ final List<Pair<Integer, Long>> listOfUsers = new ArrayList<>();
+ synchronized (mUserWakeupLock) {
+ for (int i = 0; i < mUserStarts.size(); i++) {
+ listOfUsers.add(new Pair<>(mUserStarts.keyAt(i), mUserStarts.valueAt(i)));
+ }
+ for (int i = 0; i < mStartingUsers.size(); i++) {
+ listOfUsers.add(new Pair<>(mStartingUsers.keyAt(i), mStartingUsers.valueAt(i)));
+ }
+ }
+ Collections.sort(listOfUsers, Comparator.comparingLong(pair -> pair.second));
+ for (int i = 0; i < listOfUsers.size(); i++) {
+ out.startTag(null, TAG_USER);
+ XmlUtils.writeIntAttribute(out, ATTR_USER_ID, listOfUsers.get(i).first);
+ out.endTag(null, TAG_USER);
+ }
+ out.endTag(null, TAG_USERS);
+ out.endDocument();
+ file.finishWrite(fos);
+ } catch (IOException e) {
+ Slog.wtf(USER_WAKEUP_TAG, "Error writing user wakeup data", e);
+ file.delete();
+ }
+ }
+
+ private void readUserIdList() {
+ final AtomicFile userWakeupFile = getUserWakeupFile();
+ if (userWakeupFile == null) {
+ return;
+ } else if (!userWakeupFile.exists()) {
+ Slog.w(USER_WAKEUP_TAG, "User wakeup file not available: "
+ + userWakeupFile.getBaseFile());
+ return;
+ }
+ synchronized (mUserWakeupLock) {
+ mUserStarts.clear();
+ mStartingUsers.clear();
+ }
+ try (FileInputStream fis = userWakeupFile.openRead()) {
+ final TypedXmlPullParser parser = Xml.resolvePullParser(fis);
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ // Skip
+ }
+ if (type != XmlPullParser.START_TAG) {
+ Slog.e(USER_WAKEUP_TAG, "Unable to read user list. No start tag found in "
+ + userWakeupFile.getBaseFile());
+ return;
+ }
+ int version = -1;
+ if (parser.getName().equals(TAG_USERS)) {
+ version = parser.getAttributeInt(null, ATTR_VERSION, version);
+ }
+
+ long counter = 0;
+ final long currentTime = SystemClock.elapsedRealtime();
+ // Time delay between now and first user wakeup is scheduled.
+ final long scheduleOffset = currentTime + BUFFER_TIME_MS + getUserWakeupOffset();
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (type == XmlPullParser.START_TAG) {
+ if (parser.getName().equals(TAG_USER)) {
+ final int id = parser.getAttributeInt(null, ATTR_USER_ID);
+ synchronized (mUserWakeupLock) {
+ mUserStarts.put(id, scheduleOffset + (counter++
+ * INITIAL_USER_START_SCHEDULING_DELAY_MS));
+ }
+ }
+ }
+ }
+ } catch (IOException | XmlPullParserException e) {
+ Slog.wtf(USER_WAKEUP_TAG, "Error reading user wakeup data", e);
+ }
+ }
+
+ @Nullable
+ private AtomicFile getUserWakeupFile() {
+ if (!USER_WAKEUP_DIR.exists() && !USER_WAKEUP_DIR.mkdir()) {
+ Slog.wtf(USER_WAKEUP_TAG, "Failed to mkdir() user list file: " + USER_WAKEUP_DIR);
+ return null;
+ }
+ final File userFile = new File(USER_WAKEUP_DIR, USERS_FILE_NAME);
+ return new AtomicFile(userFile);
+ }
+
+ void dump(IndentingPrintWriter pw, long nowELAPSED) {
+ synchronized (mUserWakeupLock) {
+ pw.increaseIndent();
+ pw.print("User wakeup store file path: ");
+ final AtomicFile file = getUserWakeupFile();
+ if (file == null) {
+ pw.println("null");
+ } else {
+ pw.println(file.getBaseFile().getAbsolutePath());
+ }
+ pw.println(mUserStarts.size() + " user wakeups scheduled: ");
+ for (int i = 0; i < mUserStarts.size(); i++) {
+ pw.print("UserId: ");
+ pw.print(mUserStarts.keyAt(i));
+ pw.print(", userStartTime: ");
+ TimeUtils.formatDuration(mUserStarts.valueAt(i), nowELAPSED, pw);
+ pw.println();
+ }
+ pw.println(mStartingUsers.size() + " starting users: ");
+ for (int i = 0; i < mStartingUsers.size(); i++) {
+ pw.print("UserId: ");
+ pw.print(mStartingUsers.keyAt(i));
+ pw.print(", userStartTime: ");
+ TimeUtils.formatDuration(mStartingUsers.valueAt(i), nowELAPSED, pw);
+ pw.println();
+ }
+ pw.decreaseIndent();
+ }
+ }
+}
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 5843e51bf0c9..e66f7fe195e6 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -148,6 +148,13 @@ public abstract class ActivityManagerInternal {
public abstract void onUserRemoved(@UserIdInt int userId);
/**
+ * Start user, if it is not already running, but don't bring it to foreground.
+ * @param userId ID of the user to start
+ * @return true if the user has been successfully started
+ */
+ public abstract boolean startUserInBackground(int userId);
+
+ /**
* Kill foreground apps from the specified user.
*/
public abstract void killForegroundAppsForUser(@UserIdInt int userId);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index ed1a763b1512..10ba591640cc 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -18189,6 +18189,11 @@ public class ActivityManagerService extends IActivityManager.Stub
}
@Override
+ public boolean startUserInBackground(final int userId) {
+ return ActivityManagerService.this.startUserInBackground(userId);
+ }
+
+ @Override
public void killForegroundAppsForUser(@UserIdInt int userId) {
final ArrayList<ProcessRecord> procs = new ArrayList<>();
synchronized (mProcLock) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index 99752212fcbd..ce5cee0b6113 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -137,6 +137,8 @@ import android.content.pm.PackageManagerInternal;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.Bundle;
+import android.os.Environment;
+import android.os.FileUtils;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.IBinder;
@@ -149,6 +151,7 @@ import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
@@ -159,6 +162,7 @@ import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
+import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.dx.mockito.inline.extended.MockedVoidMethod;
@@ -183,6 +187,7 @@ import com.android.server.usage.AppStandbyInternal;
import libcore.util.EmptyArray;
+import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -194,6 +199,7 @@ import org.mockito.Mock;
import org.mockito.quality.Strictness;
import org.mockito.stubbing.Answer;
+import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@@ -224,6 +230,7 @@ public final class AlarmManagerServiceTest {
private ActivityManager.UidFrozenStateChangedCallback mUidFrozenStateCallback;
private IAppOpsCallback mIAppOpsCallback;
private IAlarmManager mBinder;
+ private File mTestDir;
@Mock
private Context mMockContext;
@Mock
@@ -413,6 +420,7 @@ public final class AlarmManagerServiceTest {
.mockStatic(PermissionManagerService.class)
.mockStatic(ServiceManager.class)
.mockStatic(SystemProperties.class)
+ .mockStatic(Environment.class)
.spyStatic(UserHandle.class)
.afterSessionFinished(
() -> LocalServices.removeServiceForTest(AlarmManagerInternal.class))
@@ -429,7 +437,8 @@ public final class AlarmManagerServiceTest {
*/
private void disableFlagsNotSetByAnnotation() {
try {
- mSetFlagsRule.disableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS);
+ mSetFlagsRule.disableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS,
+ Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS);
} catch (FlagSetException fse) {
// Expected if the test about to be run requires this enabled.
}
@@ -460,7 +469,10 @@ public final class AlarmManagerServiceTest {
when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE),
eq(TEST_CALLING_USER), anyLong())).thenReturn(STANDBY_BUCKET_ACTIVE);
doReturn(Looper.getMainLooper()).when(Looper::myLooper);
-
+ mTestDir = new File(InstrumentationRegistry.getInstrumentation().getTargetContext()
+ .getFilesDir(), "alarmsTestDir");
+ mTestDir.mkdirs();
+ doReturn(mTestDir).when(Environment::getDataSystemDirectory);
when(mMockContext.getContentResolver()).thenReturn(mContentResolver);
doReturn(mDeviceConfigKeys).when(mDeviceConfigProperties).getKeyset();
@@ -579,6 +591,12 @@ public final class AlarmManagerServiceTest {
setTestableQuotas();
}
+ @After
+ public void tearDown() {
+ // Clean up test dir to remove persisted user files.
+ FileUtils.deleteContentsAndDir(mTestDir);
+ }
+
private void setTestAlarm(int type, long triggerTime, PendingIntent operation) {
setTestAlarm(type, triggerTime, operation, 0, FLAG_STANDALONE, TEST_CALLING_UID);
}
@@ -3792,6 +3810,7 @@ public final class AlarmManagerServiceTest {
}
@EnableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS)
+ @DisableFlags(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS)
@Test
public void exactListenerAlarmsRemovedOnFrozen() {
mockChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, true);
@@ -3823,6 +3842,7 @@ public final class AlarmManagerServiceTest {
}
@EnableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS)
+ @DisableFlags(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS)
@Test
public void alarmCountOnListenerFrozen() {
mockChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, true);
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
new file mode 100644
index 000000000000..5d3e4994d718
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.alarm;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.alarm.UserWakeupStore.BUFFER_TIME_MS;
+import static com.android.server.alarm.UserWakeupStore.USER_START_TIME_DEVIATION_LIMIT_MS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.SystemClock;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.os.BackgroundThread;
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.concurrent.ExecutorService;
+
+@RunWith(AndroidJUnit4.class)
+public class UserWakeupStoreTest {
+ private static final int USER_ID_1 = 10;
+ private static final int USER_ID_2 = 11;
+ private static final int USER_ID_3 = 12;
+ private static final long TEST_TIMESTAMP = 150_000;
+ private static final File TEST_SYSTEM_DIR = new File(InstrumentationRegistry
+ .getInstrumentation().getContext().getDataDir(), "alarmsTestDir");
+ private static final File ROOT_DIR = new File(TEST_SYSTEM_DIR, UserWakeupStore.ROOT_DIR_NAME);
+ private ExecutorService mMockExecutorService = null;
+ UserWakeupStore mUserWakeupStore;
+
+ @Rule
+ public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
+ .mockStatic(Environment.class)
+ .mockStatic(BackgroundThread.class)
+ .build();
+
+ @Before
+ public void setUp() {
+ TEST_SYSTEM_DIR.mkdirs();
+ doReturn(TEST_SYSTEM_DIR).when(Environment::getDataSystemDirectory);
+ mMockExecutorService = Mockito.mock(ExecutorService.class);
+ Mockito.doAnswer((invocation) -> {
+ Runnable task = invocation.getArgument(0);
+ task.run();
+ return null;
+ }).when(mMockExecutorService).execute(Mockito.any(Runnable.class));
+ doReturn(mMockExecutorService).when(BackgroundThread::getExecutor);
+ mUserWakeupStore = new UserWakeupStore();
+ spyOn(mUserWakeupStore);
+ mUserWakeupStore.init();
+ }
+
+ @After
+ public void tearDown() {
+ // Clean up test dir to remove persisted user files.
+ FileUtils.deleteContentsAndDir(TEST_SYSTEM_DIR);
+ }
+
+ @Test
+ public void testAddWakeups() {
+ mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 19_000);
+ mUserWakeupStore.addUserWakeup(USER_ID_2, TEST_TIMESTAMP - 7_000);
+ mUserWakeupStore.addUserWakeup(USER_ID_3, TEST_TIMESTAMP - 13_000);
+ assertEquals(3, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length);
+ ArrayList<Integer> userIds = new ArrayList<>();
+ userIds.add(USER_ID_1);
+ userIds.add(USER_ID_2);
+ userIds.add(USER_ID_3);
+ final int[] usersToWakeup = mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP);
+ ArrayList<Integer> userWakeups = new ArrayList<>();
+ for (int i = 0; i < usersToWakeup.length; i++) {
+ userWakeups.add(usersToWakeup[i]);
+ }
+ Collections.sort(userIds);
+ Collections.sort(userWakeups);
+ assertEquals(userIds, userWakeups);
+
+ final File file = new File(ROOT_DIR , "usersWithAlarmClocks.xml");
+ assertTrue(file.exists());
+ }
+
+ @Test
+ public void testAddMultipleWakeupsForUser_ensureOnlyLastWakeupRemains() {
+ final long finalAlarmTime = TEST_TIMESTAMP - 13_000;
+ mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 29_000);
+ mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 7_000);
+ mUserWakeupStore.addUserWakeup(USER_ID_1, finalAlarmTime);
+ assertEquals(1, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length);
+ final long alarmTime = mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_1)
+ + BUFFER_TIME_MS;
+ assertTrue(finalAlarmTime + USER_START_TIME_DEVIATION_LIMIT_MS >= alarmTime);
+ assertTrue(finalAlarmTime - USER_START_TIME_DEVIATION_LIMIT_MS <= alarmTime);
+ }
+
+ @Test
+ public void testRemoveWakeupForUser_negativeWakeupTimeIsReturnedForUser() {
+ mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 19_000);
+ mUserWakeupStore.addUserWakeup(USER_ID_2, TEST_TIMESTAMP - 7_000);
+ mUserWakeupStore.addUserWakeup(USER_ID_3, TEST_TIMESTAMP - 13_000);
+ assertEquals(3, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length);
+ mUserWakeupStore.removeUserWakeup(USER_ID_3);
+ assertEquals(-1, mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_3));
+ assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_2) > 0);
+ }
+
+ @Test
+ public void testGetNextUserWakeup() {
+ mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 19_000);
+ mUserWakeupStore.addUserWakeup(USER_ID_2, TEST_TIMESTAMP - 3_000);
+ mUserWakeupStore.addUserWakeup(USER_ID_3, TEST_TIMESTAMP - 13_000);
+ assertEquals(mUserWakeupStore.getNextWakeupTime(),
+ mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_1));
+ mUserWakeupStore.removeUserWakeup(USER_ID_1);
+ assertEquals(mUserWakeupStore.getNextWakeupTime(),
+ mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_3));
+ }
+
+ @Test
+ public void testWriteAndReadUsersFromFile() {
+ mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 19_000);
+ mUserWakeupStore.addUserWakeup(USER_ID_2, TEST_TIMESTAMP - 7_000);
+ mUserWakeupStore.addUserWakeup(USER_ID_3, TEST_TIMESTAMP - 13_000);
+ assertEquals(3, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length);
+ mUserWakeupStore.init();
+ final long realtime = SystemClock.elapsedRealtime();
+ assertEquals(0, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length);
+ assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_2) > realtime);
+ assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_1)
+ < mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_3));
+ assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_3)
+ < mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_2));
+ assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_1) - realtime
+ < BUFFER_TIME_MS + USER_START_TIME_DEVIATION_LIMIT_MS);
+ assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_3) - realtime
+ < 2 * BUFFER_TIME_MS + USER_START_TIME_DEVIATION_LIMIT_MS);
+ assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_2) - realtime
+ < 3 * BUFFER_TIME_MS + USER_START_TIME_DEVIATION_LIMIT_MS);
+ }
+ //TODO: b/330264023 - Add tests for I/O in usersWithAlarmClocks.xml.
+}