1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.alarm;
18 
19 
20 import android.annotation.Nullable;
21 import android.os.Environment;
22 import android.os.SystemClock;
23 import android.util.AtomicFile;
24 import android.util.IndentingPrintWriter;
25 import android.util.Pair;
26 import android.util.Slog;
27 import android.util.SparseLongArray;
28 import android.util.TimeUtils;
29 import android.util.Xml;
30 
31 import com.android.internal.annotations.GuardedBy;
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.internal.os.BackgroundThread;
34 import com.android.internal.util.FastXmlSerializer;
35 import com.android.internal.util.XmlUtils;
36 import com.android.modules.utils.TypedXmlPullParser;
37 
38 import org.xmlpull.v1.XmlPullParser;
39 import org.xmlpull.v1.XmlPullParserException;
40 import org.xmlpull.v1.XmlSerializer;
41 
42 import java.io.File;
43 import java.io.FileInputStream;
44 import java.io.FileOutputStream;
45 import java.io.IOException;
46 import java.nio.charset.StandardCharsets;
47 import java.util.ArrayList;
48 import java.util.Arrays;
49 import java.util.Collections;
50 import java.util.Comparator;
51 import java.util.List;
52 import java.util.Random;
53 import java.util.concurrent.Executor;
54 import java.util.concurrent.TimeUnit;
55 
56 /**
57  * User wakeup store keeps the list of user ids with the times that user needs to be started in
58  * sorted list in order for alarms to execute even if user gets stopped.
59  * The list of user ids with at least one alarms scheduled is also persisted to the XML file to
60  * start them after the device reboot.
61  */
62 public class UserWakeupStore {
63     private static final boolean DEBUG = false;
64 
65     static final String USER_WAKEUP_TAG = UserWakeupStore.class.getSimpleName();
66     private static final String TAG_USERS = "users";
67     private static final String TAG_USER = "user";
68     private static final String ATTR_USER_ID = "user_id";
69     private static final String ATTR_VERSION = "version";
70 
71     public static final int XML_VERSION_CURRENT = 1;
72     @VisibleForTesting
73     static final String ROOT_DIR_NAME = "alarms";
74     @VisibleForTesting
75     static final String USERS_FILE_NAME = "usersWithAlarmClocks.xml";
76 
77     /**
78      * Time offset of user start before the original alarm time in milliseconds.
79      * Also used to schedule user start after reboot to avoid starting them simultaneously.
80      */
81     @VisibleForTesting
82     static final long BUFFER_TIME_MS = TimeUnit.SECONDS.toMillis(30);
83     /**
84      * Maximum time deviation limit to introduce a 5-second time window for user starts.
85      */
86     @VisibleForTesting
87     static final long USER_START_TIME_DEVIATION_LIMIT_MS = TimeUnit.SECONDS.toMillis(5);
88     /**
89      * Delay between two consecutive user starts scheduled during user wakeup store initialization.
90      */
91     @VisibleForTesting
92     static final long INITIAL_USER_START_SCHEDULING_DELAY_MS = TimeUnit.SECONDS.toMillis(5);
93 
94     private final Object mUserWakeupLock = new Object();
95 
96     /**
97      * A list of wakeups for users with scheduled alarms.
98      */
99     @GuardedBy("mUserWakeupLock")
100     private final SparseLongArray mUserStarts = new SparseLongArray();
101     /**
102      * A list of users that are in a phase after they have been started but before alarms were
103      * initialized.
104      */
105     @GuardedBy("mUserWakeupLock")
106     private final SparseLongArray mStartingUsers = new SparseLongArray();
107     private Executor mBackgroundExecutor;
108     private static final File USER_WAKEUP_DIR = new File(Environment.getDataSystemDirectory(),
109             ROOT_DIR_NAME);
110     private static final Random sRandom = new Random(500);
111 
112     /**
113      * Initialize mUserWakeups with persisted values.
114      */
init()115     public void init() {
116         mBackgroundExecutor = BackgroundThread.getExecutor();
117         mBackgroundExecutor.execute(this::readUserIdList);
118     }
119 
120     /**
121      * Add user wakeup for the alarm.
122      * @param userId Id of the user that scheduled alarm.
123      * @param alarmTime time when alarm is expected to trigger.
124      */
addUserWakeup(int userId, long alarmTime)125     public void addUserWakeup(int userId, long alarmTime) {
126         synchronized (mUserWakeupLock) {
127             // This should not be needed, but if an app in the user is scheduling an alarm clock, we
128             // consider the user start complete. There is a dedicated removal when user is started.
129             mStartingUsers.delete(userId);
130             mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset());
131         }
132         updateUserListFile();
133     }
134 
135     /**
136      * Remove wakeup scheduled for the user with given userId if present.
137      */
removeUserWakeup(int userId)138     public void removeUserWakeup(int userId) {
139         if (deleteWakeupFromUserStarts(userId)) {
140             updateUserListFile();
141         }
142     }
143 
144     /**
145      * Get ids of users that need to be started now.
146      * @param nowElapsed current time.
147      * @return user ids to be started, or empty if no user needs to be started.
148      */
getUserIdsToWakeup(long nowElapsed)149     public int[] getUserIdsToWakeup(long nowElapsed) {
150         synchronized (mUserWakeupLock) {
151             final int[] userIds = new int[mUserStarts.size()];
152             int index = 0;
153             for (int i = mUserStarts.size() - 1; i >= 0; i--) {
154                 if (mUserStarts.valueAt(i) <= nowElapsed) {
155                     userIds[index++] = mUserStarts.keyAt(i);
156                 }
157             }
158             return Arrays.copyOfRange(userIds, 0, index);
159         }
160     }
161 
162     /**
163      * Persist user ids that have alarms scheduled so that they can be started after device reboot.
164      */
updateUserListFile()165     private void updateUserListFile() {
166         mBackgroundExecutor.execute(() -> {
167             try {
168                 writeUserIdList();
169                 if (DEBUG) {
170                     synchronized (mUserWakeupLock) {
171                         Slog.i(USER_WAKEUP_TAG, "Printing out user wakeups " + mUserStarts.size());
172                         for (int i = 0; i < mUserStarts.size(); i++) {
173                             Slog.i(USER_WAKEUP_TAG, "User id: " + mUserStarts.keyAt(i) + "  time: "
174                                     + mUserStarts.valueAt(i));
175                         }
176                     }
177                 }
178             } catch (Exception e) {
179                 Slog.e(USER_WAKEUP_TAG, "Failed to write " + e.getLocalizedMessage());
180             }
181         });
182     }
183 
184     /**
185      * Return scheduled start time for user or -1 if user does not have alarm set.
186      */
187     @VisibleForTesting
getWakeupTimeForUser(int userId)188     long getWakeupTimeForUser(int userId) {
189         synchronized (mUserWakeupLock) {
190             return mUserStarts.get(userId, -1);
191         }
192     }
193 
194     /**
195      * Move user from wakeup list to starting user list.
196      */
onUserStarting(int userId)197     public void onUserStarting(int userId) {
198         synchronized (mUserWakeupLock) {
199             final long wakeup = getWakeupTimeForUser(userId);
200             if (wakeup >= 0) {
201                 mStartingUsers.put(userId, wakeup);
202                 mUserStarts.delete(userId);
203             }
204         }
205     }
206 
207     /**
208      * Remove userId from starting user list once start is complete.
209      */
onUserStarted(int userId)210     public void onUserStarted(int userId) {
211         if (deleteWakeupFromStartingUsers(userId)) {
212             updateUserListFile();
213         }
214     }
215 
216     /**
217      * Remove userId from the store when the user is removed.
218      */
onUserRemoved(int userId)219     public void onUserRemoved(int userId) {
220         if (deleteWakeupFromUserStarts(userId) || deleteWakeupFromStartingUsers(userId)) {
221             updateUserListFile();
222         }
223     }
224 
225     /**
226      * Remove wakeup for a given userId from mUserStarts.
227      * @return true if an entry is found and the list of wakeups changes.
228      */
deleteWakeupFromUserStarts(int userId)229     private boolean deleteWakeupFromUserStarts(int userId) {
230         int index;
231         synchronized (mUserWakeupLock) {
232             index = mUserStarts.indexOfKey(userId);
233             if (index >= 0) {
234                 mUserStarts.removeAt(index);
235             }
236         }
237         return index >= 0;
238     }
239 
240     /**
241      * Remove wakeup for a given userId from mStartingUsers.
242      * @return true if an entry is found and the list of wakeups changes.
243      */
deleteWakeupFromStartingUsers(int userId)244     private boolean deleteWakeupFromStartingUsers(int userId) {
245         int index;
246         synchronized (mUserWakeupLock) {
247             index = mStartingUsers.indexOfKey(userId);
248             if (index >= 0) {
249                 mStartingUsers.removeAt(index);
250             }
251         }
252         return index >= 0;
253     }
254 
255     /**
256      * Get the soonest wakeup time in the store.
257      */
getNextWakeupTime()258     public long getNextWakeupTime() {
259         long nextWakeupTime = -1;
260         synchronized (mUserWakeupLock) {
261             for (int i = 0; i < mUserStarts.size(); i++) {
262                 if (mUserStarts.valueAt(i) < nextWakeupTime || nextWakeupTime == -1) {
263                     nextWakeupTime = mUserStarts.valueAt(i);
264                 }
265             }
266         }
267         return nextWakeupTime;
268     }
269 
getUserWakeupOffset()270     private static long getUserWakeupOffset() {
271         return sRandom.nextLong(USER_START_TIME_DEVIATION_LIMIT_MS * 2)
272                 - USER_START_TIME_DEVIATION_LIMIT_MS;
273     }
274 
275     /**
276      * Write a list of ids for users who have alarm scheduled.
277      * Sample XML file:
278      *
279      * <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
280      * <users version="1">
281      * <user user_id="12" />
282      * <user user_id="10" />
283      * </users>
284      * ~
285      */
writeUserIdList()286     private void writeUserIdList() {
287         final AtomicFile file = getUserWakeupFile();
288         if (file == null) {
289             return;
290         }
291         try (FileOutputStream fos = file.startWrite(SystemClock.uptimeMillis())) {
292             final XmlSerializer out = new FastXmlSerializer();
293             out.setOutput(fos, StandardCharsets.UTF_8.name());
294             out.startDocument(null, true);
295             out.startTag(null, TAG_USERS);
296             XmlUtils.writeIntAttribute(out, ATTR_VERSION, XML_VERSION_CURRENT);
297             final List<Pair<Integer, Long>> listOfUsers = new ArrayList<>();
298             synchronized (mUserWakeupLock) {
299                 for (int i = 0; i < mUserStarts.size(); i++) {
300                     listOfUsers.add(new Pair<>(mUserStarts.keyAt(i), mUserStarts.valueAt(i)));
301                 }
302                 for (int i = 0; i < mStartingUsers.size(); i++) {
303                     listOfUsers.add(new Pair<>(mStartingUsers.keyAt(i), mStartingUsers.valueAt(i)));
304                 }
305             }
306             Collections.sort(listOfUsers, Comparator.comparingLong(pair -> pair.second));
307             for (int i = 0; i < listOfUsers.size(); i++) {
308                 out.startTag(null, TAG_USER);
309                 XmlUtils.writeIntAttribute(out, ATTR_USER_ID, listOfUsers.get(i).first);
310                 out.endTag(null, TAG_USER);
311             }
312             out.endTag(null, TAG_USERS);
313             out.endDocument();
314             file.finishWrite(fos);
315         } catch (IOException e) {
316             Slog.wtf(USER_WAKEUP_TAG, "Error writing user wakeup data", e);
317             file.delete();
318         }
319     }
320 
readUserIdList()321     private void readUserIdList() {
322         final AtomicFile userWakeupFile = getUserWakeupFile();
323         if (userWakeupFile == null) {
324             return;
325         } else if (!userWakeupFile.exists()) {
326             Slog.w(USER_WAKEUP_TAG, "User wakeup file not available: "
327                     + userWakeupFile.getBaseFile());
328             return;
329         }
330         synchronized (mUserWakeupLock) {
331             mUserStarts.clear();
332             mStartingUsers.clear();
333         }
334         try (FileInputStream fis = userWakeupFile.openRead()) {
335             final TypedXmlPullParser parser = Xml.resolvePullParser(fis);
336             int type;
337             while ((type = parser.next()) != XmlPullParser.START_TAG
338                     && type != XmlPullParser.END_DOCUMENT) {
339                 // Skip
340             }
341             if (type != XmlPullParser.START_TAG) {
342                 Slog.e(USER_WAKEUP_TAG, "Unable to read user list. No start tag found in "
343                         + userWakeupFile.getBaseFile());
344                 return;
345             }
346             int version = -1;
347             if (parser.getName().equals(TAG_USERS)) {
348                 version = parser.getAttributeInt(null, ATTR_VERSION, version);
349             }
350 
351             long counter = 0;
352             final long currentTime = SystemClock.elapsedRealtime();
353             // Time delay between now and first user wakeup is scheduled.
354             final long scheduleOffset = currentTime + BUFFER_TIME_MS + getUserWakeupOffset();
355             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
356                 if (type == XmlPullParser.START_TAG) {
357                     if (parser.getName().equals(TAG_USER)) {
358                         final int id = parser.getAttributeInt(null, ATTR_USER_ID);
359                         synchronized (mUserWakeupLock) {
360                             mUserStarts.put(id, scheduleOffset + (counter++
361                                     * INITIAL_USER_START_SCHEDULING_DELAY_MS));
362                         }
363                     }
364                 }
365             }
366         } catch (IOException | XmlPullParserException e) {
367             Slog.wtf(USER_WAKEUP_TAG, "Error reading user wakeup data", e);
368         }
369     }
370 
371     @Nullable
getUserWakeupFile()372     private AtomicFile getUserWakeupFile() {
373         if (!USER_WAKEUP_DIR.exists() && !USER_WAKEUP_DIR.mkdir()) {
374             Slog.wtf(USER_WAKEUP_TAG, "Failed to mkdir() user list file: " + USER_WAKEUP_DIR);
375             return null;
376         }
377         final File userFile = new File(USER_WAKEUP_DIR, USERS_FILE_NAME);
378         return new AtomicFile(userFile);
379     }
380 
dump(IndentingPrintWriter pw, long nowELAPSED)381     void dump(IndentingPrintWriter pw, long nowELAPSED) {
382         synchronized (mUserWakeupLock) {
383             pw.increaseIndent();
384             pw.print("User wakeup store file path: ");
385             final AtomicFile file = getUserWakeupFile();
386             if (file == null) {
387                 pw.println("null");
388             } else {
389                 pw.println(file.getBaseFile().getAbsolutePath());
390             }
391             pw.println(mUserStarts.size() + " user wakeups scheduled: ");
392             for (int i = 0; i < mUserStarts.size(); i++) {
393                 pw.print("UserId: ");
394                 pw.print(mUserStarts.keyAt(i));
395                 pw.print(", userStartTime: ");
396                 TimeUtils.formatDuration(mUserStarts.valueAt(i), nowELAPSED, pw);
397                 pw.println();
398             }
399             pw.println(mStartingUsers.size() + " starting users: ");
400             for (int i = 0; i < mStartingUsers.size(); i++) {
401                 pw.print("UserId: ");
402                 pw.print(mStartingUsers.keyAt(i));
403                 pw.print(", userStartTime: ");
404                 TimeUtils.formatDuration(mStartingUsers.valueAt(i), nowELAPSED, pw);
405                 pw.println();
406             }
407             pw.decreaseIndent();
408         }
409     }
410 }
411