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