diff options
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. +} |