Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8ca5a040 authored by Tetiana Meronyk's avatar Tetiana Meronyk
Browse files

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
parent 42910f6b
Loading
Loading
Loading
Loading
+67 −1
Original line number Diff line number Diff line
@@ -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);
+381 −0
Original line number Diff line number Diff line
/*
 * 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();
        }
    }
}
+7 −0
Original line number Diff line number Diff line
@@ -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.
     */
+5 −0
Original line number Diff line number Diff line
@@ -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<ProcessRecord> procs = new ArrayList<>();
+22 −2

File changed.

Preview size limit exceeded, changes collapsed.

Loading