• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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     private Executor mBackgroundExecutor;
103     private static final File USER_WAKEUP_DIR = new File(Environment.getDataSystemDirectory(),
104             ROOT_DIR_NAME);
105     private static final Random sRandom = new Random(500);
106 
107     /**
108      * Initialize mUserWakeups with persisted values.
109      */
init()110     public void init() {
111         mBackgroundExecutor = BackgroundThread.getExecutor();
112         mBackgroundExecutor.execute(this::readUserIdList);
113     }
114 
115     /**
116      * Add user wakeup for the alarm if needed.
117      * @param userId Id of the user that scheduled alarm.
118      * @param alarmTime time when alarm is expected to trigger.
119      */
addUserWakeup(int userId, long alarmTime)120     public void addUserWakeup(int userId, long alarmTime) {
121         synchronized (mUserWakeupLock) {
122             mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset());
123         }
124         updateUserListFile();
125     }
126 
127     /**
128      * Remove wakeup scheduled for the user with given userId if present.
129      */
removeUserWakeup(int userId)130     public void removeUserWakeup(int userId) {
131         if (deleteWakeupFromUserStarts(userId)) {
132             updateUserListFile();
133         }
134     }
135 
136     /**
137      * Get ids of users that need to be started now.
138      * @param nowElapsed current time.
139      * @return user ids to be started, or empty if no user needs to be started.
140      */
getUserIdsToWakeup(long nowElapsed)141     public int[] getUserIdsToWakeup(long nowElapsed) {
142         synchronized (mUserWakeupLock) {
143             final int[] userIds = new int[mUserStarts.size()];
144             int index = 0;
145             for (int i = mUserStarts.size() - 1; i >= 0; i--) {
146                 if (mUserStarts.valueAt(i) <= nowElapsed) {
147                     userIds[index++] = mUserStarts.keyAt(i);
148                 }
149             }
150             return Arrays.copyOfRange(userIds, 0, index);
151         }
152     }
153 
154     /**
155      * Persist user ids that have alarms scheduled so that they can be started after device reboot.
156      */
updateUserListFile()157     private void updateUserListFile() {
158         mBackgroundExecutor.execute(() -> {
159             try {
160                 writeUserIdList();
161                 if (DEBUG) {
162                     synchronized (mUserWakeupLock) {
163                         Slog.i(USER_WAKEUP_TAG, "Printing out user wakeups " + mUserStarts.size());
164                         for (int i = 0; i < mUserStarts.size(); i++) {
165                             Slog.i(USER_WAKEUP_TAG, "User id: " + mUserStarts.keyAt(i) + "  time: "
166                                     + mUserStarts.valueAt(i));
167                         }
168                     }
169                 }
170             } catch (Exception e) {
171                 Slog.e(USER_WAKEUP_TAG, "Failed to write " + e.getLocalizedMessage());
172             }
173         });
174     }
175 
176     /**
177      * Return scheduled start time for user or -1 if user does not have alarm set.
178      */
179     @VisibleForTesting
getWakeupTimeForUser(int userId)180     long getWakeupTimeForUser(int userId) {
181         synchronized (mUserWakeupLock) {
182             return mUserStarts.get(userId, -1);
183         }
184     }
185 
186     /**
187      * Remove scheduled user wakeup from the list when it is started.
188      */
onUserStarting(int userId)189     public void onUserStarting(int userId) {
190         if (deleteWakeupFromUserStarts(userId)) {
191             updateUserListFile();
192         }
193     }
194 
195     /**
196      * Remove userId from the store when the user is removed.
197      */
onUserRemoved(int userId)198     public void onUserRemoved(int userId) {
199         if (deleteWakeupFromUserStarts(userId)) {
200             updateUserListFile();
201         }
202     }
203 
204     /**
205      * Remove wakeup for a given userId from mUserStarts.
206      * @return true if an entry is found and the list of wakeups changes.
207      */
deleteWakeupFromUserStarts(int userId)208     private boolean deleteWakeupFromUserStarts(int userId) {
209         synchronized (mUserWakeupLock) {
210             final int index = mUserStarts.indexOfKey(userId);
211             if (index >= 0) {
212                 mUserStarts.removeAt(index);
213                 return true;
214             }
215             return false;
216         }
217     }
218 
219     /**
220      * Get the soonest wakeup time in the store.
221      */
getNextWakeupTime()222     public long getNextWakeupTime() {
223         long nextWakeupTime = -1;
224         synchronized (mUserWakeupLock) {
225             for (int i = 0; i < mUserStarts.size(); i++) {
226                 if (mUserStarts.valueAt(i) < nextWakeupTime || nextWakeupTime == -1) {
227                     nextWakeupTime = mUserStarts.valueAt(i);
228                 }
229             }
230         }
231         return nextWakeupTime;
232     }
233 
getUserWakeupOffset()234     private static long getUserWakeupOffset() {
235         return sRandom.nextLong(USER_START_TIME_DEVIATION_LIMIT_MS * 2)
236                 - USER_START_TIME_DEVIATION_LIMIT_MS;
237     }
238 
239     /**
240      * Write a list of ids for users who have alarm scheduled.
241      * Sample XML file:
242      *
243      * <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
244      * <users version="1">
245      * <user user_id="12" />
246      * <user user_id="10" />
247      * </users>
248      * ~
249      */
writeUserIdList()250     private void writeUserIdList() {
251         final AtomicFile file = getUserWakeupFile();
252         if (file == null) {
253             return;
254         }
255         try (FileOutputStream fos = file.startWrite(SystemClock.uptimeMillis())) {
256             final XmlSerializer out = new FastXmlSerializer();
257             out.setOutput(fos, StandardCharsets.UTF_8.name());
258             out.startDocument(null, true);
259             out.startTag(null, TAG_USERS);
260             XmlUtils.writeIntAttribute(out, ATTR_VERSION, XML_VERSION_CURRENT);
261             final List<Pair<Integer, Long>> listOfUsers = new ArrayList<>();
262             synchronized (mUserWakeupLock) {
263                 for (int i = 0; i < mUserStarts.size(); i++) {
264                     listOfUsers.add(new Pair<>(mUserStarts.keyAt(i), mUserStarts.valueAt(i)));
265                 }
266             }
267             Collections.sort(listOfUsers, Comparator.comparingLong(pair -> pair.second));
268             for (int i = 0; i < listOfUsers.size(); i++) {
269                 out.startTag(null, TAG_USER);
270                 XmlUtils.writeIntAttribute(out, ATTR_USER_ID, listOfUsers.get(i).first);
271                 out.endTag(null, TAG_USER);
272             }
273             out.endTag(null, TAG_USERS);
274             out.endDocument();
275             file.finishWrite(fos);
276         } catch (IOException e) {
277             Slog.wtf(USER_WAKEUP_TAG, "Error writing user wakeup data", e);
278             file.delete();
279         }
280     }
281 
readUserIdList()282     private void readUserIdList() {
283         final AtomicFile userWakeupFile = getUserWakeupFile();
284         if (userWakeupFile == null) {
285             return;
286         } else if (!userWakeupFile.exists()) {
287             Slog.w(USER_WAKEUP_TAG, "User wakeup file not available: "
288                     + userWakeupFile.getBaseFile());
289             return;
290         }
291         synchronized (mUserWakeupLock) {
292             mUserStarts.clear();
293         }
294         try (FileInputStream fis = userWakeupFile.openRead()) {
295             final TypedXmlPullParser parser = Xml.resolvePullParser(fis);
296             int type;
297             while ((type = parser.next()) != XmlPullParser.START_TAG
298                     && type != XmlPullParser.END_DOCUMENT) {
299                 // Skip
300             }
301             if (type != XmlPullParser.START_TAG) {
302                 Slog.e(USER_WAKEUP_TAG, "Unable to read user list. No start tag found in "
303                         + userWakeupFile.getBaseFile());
304                 return;
305             }
306             int version = -1;
307             if (parser.getName().equals(TAG_USERS)) {
308                 version = parser.getAttributeInt(null, ATTR_VERSION, version);
309             }
310 
311             long counter = 0;
312             final long currentTime = SystemClock.elapsedRealtime();
313             // Time delay between now and first user wakeup is scheduled.
314             final long scheduleOffset = currentTime + BUFFER_TIME_MS + getUserWakeupOffset();
315             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
316                 if (type == XmlPullParser.START_TAG) {
317                     if (parser.getName().equals(TAG_USER)) {
318                         final int id = parser.getAttributeInt(null, ATTR_USER_ID);
319                         synchronized (mUserWakeupLock) {
320                             mUserStarts.put(id, scheduleOffset + (counter++
321                                     * INITIAL_USER_START_SCHEDULING_DELAY_MS));
322                         }
323                     }
324                 }
325             }
326         } catch (IOException | XmlPullParserException e) {
327             Slog.wtf(USER_WAKEUP_TAG, "Error reading user wakeup data", e);
328         }
329     }
330 
331     @Nullable
getUserWakeupFile()332     private AtomicFile getUserWakeupFile() {
333         if (!USER_WAKEUP_DIR.exists() && !USER_WAKEUP_DIR.mkdir()) {
334             Slog.wtf(USER_WAKEUP_TAG, "Failed to mkdir() user list file: " + USER_WAKEUP_DIR);
335             return null;
336         }
337         final File userFile = new File(USER_WAKEUP_DIR, USERS_FILE_NAME);
338         return new AtomicFile(userFile);
339     }
340 
dump(IndentingPrintWriter pw, long nowELAPSED)341     void dump(IndentingPrintWriter pw, long nowELAPSED) {
342         synchronized (mUserWakeupLock) {
343             pw.increaseIndent();
344             pw.print("User wakeup store file path: ");
345             final AtomicFile file = getUserWakeupFile();
346             if (file == null) {
347                 pw.println("null");
348             } else {
349                 pw.println(file.getBaseFile().getAbsolutePath());
350             }
351             pw.println(mUserStarts.size() + " user wakeups scheduled: ");
352             for (int i = 0; i < mUserStarts.size(); i++) {
353                 pw.print("UserId: ");
354                 pw.print(mUserStarts.keyAt(i));
355                 pw.print(", userStartTime: ");
356                 TimeUtils.formatDuration(mUserStarts.valueAt(i), nowELAPSED, pw);
357                 pw.println();
358             }
359             pw.decreaseIndent();
360         }
361     }
362 }
363