From 8ca5a04051c28cda09ce886e85e42c88541e043b Mon Sep 17 00:00:00 2001 From: Tetiana Meronyk Date: Fri, 2 Feb 2024 21:41:58 +0000 Subject: Implement user start before alarms go off Before this change alarms in multiuser environment were not consistent. If the user was stopped, their alarm did not go off. Since user stops happen without user's explicit interaction and their status is not available to regular users, if the alarm was set on background user, there was no certainty that the alarm would ring. After the change a list of users with alarms scheduled is stored to start them in background shortly before their alarm. This ensures consistency in alarms going off even if the user gets stopped. Persistence of this list was added to save this list even after device reboots. Bug: 314907186 Test: atest AlarmManagerServiceTest && atest UserWakeupStoreTest Change-Id: I5a75813d76f505383909ac6a281902c54784a1ed --- .../android/server/alarm/AlarmManagerService.java | 68 +++- .../com/android/server/alarm/UserWakeupStore.java | 381 +++++++++++++++++++++ core/java/android/app/ActivityManagerInternal.java | 7 + .../android/server/am/ActivityManagerService.java | 5 + .../server/alarm/AlarmManagerServiceTest.java | 24 +- .../android/server/alarm/UserWakeupStoreTest.java | 170 +++++++++ 6 files changed, 652 insertions(+), 3 deletions(-) create mode 100644 apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java create mode 100644 services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java 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> 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: + * + * + * + * + * + * + * ~ + */ + 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> 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 e28a6ce9b423..2102274de253 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -147,6 +147,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. */ diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 5e6ff55f4e94..d6180f4bcbae 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -18135,6 +18135,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 procs = new ArrayList<>(); 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 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 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. +} -- cgit v1.2.3-59-g8ed1b