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