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.net.module.util; 18 19 import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR; 20 import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT; 21 22 import android.os.Handler; 23 import android.os.Looper; 24 import android.os.Message; 25 import android.os.MessageQueue; 26 import android.os.ParcelFileDescriptor; 27 import android.os.SystemClock; 28 import android.system.ErrnoException; 29 import android.system.Os; 30 import android.util.CloseGuard; 31 import android.util.Log; 32 33 import androidx.annotation.NonNull; 34 35 import java.io.IOException; 36 import java.util.PriorityQueue; 37 38 /** 39 * Represents a realtime scheduler object used for scheduling tasks with precise delays. 40 * Compared to {@link Handler#postDelayed}, this class offers enhanced accuracy for delayed 41 * callbacks by accounting for periods when the device is in deep sleep. 42 * 43 * <p> This class is designed for use exclusively from the handler thread. 44 * 45 * **Usage Examples:** 46 * 47 * ** Scheduling recurring tasks with the same RealtimeScheduler ** 48 * 49 * ```java 50 * // Create a RealtimeScheduler 51 * final RealtimeScheduler scheduler = new RealtimeScheduler(handler); 52 * 53 * // Schedule a new task with a delay. 54 * scheduler.postDelayed(() -> taskToExecute(), delayTime); 55 * 56 * // Once the delay has elapsed, and the task is running, schedule another task. 57 * scheduler.postDelayed(() -> anotherTaskToExecute(), anotherDelayTime); 58 * 59 * // Remember to close the RealtimeScheduler after all tasks have finished running. 60 * scheduler.close(); 61 * ``` 62 */ 63 public class RealtimeScheduler { 64 private static final String TAG = RealtimeScheduler.class.getSimpleName(); 65 // EVENT_ERROR may be generated even if not specified, as per its javadoc. 66 private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR; 67 private final CloseGuard mGuard = new CloseGuard(); 68 @NonNull 69 private final Handler mHandler; 70 @NonNull 71 private final MessageQueue mQueue; 72 @NonNull 73 private final ParcelFileDescriptor mParcelFileDescriptor; 74 private final int mFdInt; 75 76 private final PriorityQueue<Task> mTaskQueue; 77 78 /** 79 * An abstract class for defining tasks that can be executed using a {@link Handler}. 80 */ 81 private abstract static class Task implements Comparable<Task> { 82 private final long mRunTimeMs; 83 private final long mCreatedTimeNs = SystemClock.elapsedRealtimeNanos(); 84 85 /** 86 * create a task with a run time 87 */ Task(long runTimeMs)88 Task(long runTimeMs) { 89 mRunTimeMs = runTimeMs; 90 } 91 92 /** 93 * Executes the task using the provided {@link Handler}. 94 * 95 * @param handler The {@link Handler} to use for executing the task. 96 */ post(Handler handler)97 abstract void post(Handler handler); 98 99 @Override compareTo(@onNull Task o)100 public int compareTo(@NonNull Task o) { 101 if (mRunTimeMs != o.mRunTimeMs) { 102 return Long.compare(mRunTimeMs, o.mRunTimeMs); 103 } 104 return Long.compare(mCreatedTimeNs, o.mCreatedTimeNs); 105 } 106 107 /** 108 * Returns the run time of the task. 109 */ getRunTimeMs()110 public long getRunTimeMs() { 111 return mRunTimeMs; 112 } 113 } 114 115 /** 116 * A task that sends a {@link Message} using a {@link Handler}. 117 */ 118 private static class MessageTask extends Task { 119 private final Message mMessage; 120 MessageTask(Message message, long runTimeMs)121 MessageTask(Message message, long runTimeMs) { 122 super(runTimeMs); 123 mMessage = message; 124 } 125 126 /** 127 * Sends the {@link Message} using the provided {@link Handler}. 128 * 129 * @param handler The {@link Handler} to use for sending the message. 130 */ 131 @Override post(Handler handler)132 public void post(Handler handler) { 133 handler.sendMessage(mMessage); 134 } 135 } 136 137 /** 138 * A task that posts a {@link Runnable} to a {@link Handler}. 139 */ 140 private static class RunnableTask extends Task { 141 private final Runnable mRunnable; 142 RunnableTask(Runnable runnable, long runTimeMs)143 RunnableTask(Runnable runnable, long runTimeMs) { 144 super(runTimeMs); 145 mRunnable = runnable; 146 } 147 148 /** 149 * Posts the {@link Runnable} to the provided {@link Handler}. 150 * 151 * @param handler The {@link Handler} to use for posting the runnable. 152 */ 153 @Override post(Handler handler)154 public void post(Handler handler) { 155 handler.post(mRunnable); 156 } 157 } 158 159 /** 160 * The RealtimeScheduler constructor 161 * 162 * Note: The constructor is currently safe to call on another thread because it only sets final 163 * members and registers the event to be called on the handler. 164 */ RealtimeScheduler(@onNull Handler handler)165 public RealtimeScheduler(@NonNull Handler handler) { 166 mFdInt = TimerFdUtils.createTimerFileDescriptor(); 167 mParcelFileDescriptor = ParcelFileDescriptor.adoptFd(mFdInt); 168 mHandler = handler; 169 mQueue = handler.getLooper().getQueue(); 170 mTaskQueue = new PriorityQueue<>(); 171 registerFdEventListener(); 172 173 mGuard.open("close"); 174 } 175 enqueueTask(@onNull Task task, long delayMs)176 private boolean enqueueTask(@NonNull Task task, long delayMs) { 177 ensureRunningOnCorrectThread(); 178 if (delayMs <= 0L) { 179 task.post(mHandler); 180 return true; 181 } 182 if (mTaskQueue.isEmpty() || task.compareTo(mTaskQueue.peek()) < 0) { 183 if (!TimerFdUtils.setExpirationTime(mFdInt, delayMs)) { 184 return false; 185 } 186 } 187 mTaskQueue.add(task); 188 return true; 189 } 190 191 /** 192 * Set a runnable to be executed after a specified delay. 193 * 194 * If delayMs is less than or equal to 0, the runnable will be executed immediately. 195 * 196 * @param runnable the runnable to be executed 197 * @param delayMs the delay time in milliseconds 198 * @return true if the task is scheduled successfully, false otherwise. 199 */ postDelayed(@onNull Runnable runnable, long delayMs)200 public boolean postDelayed(@NonNull Runnable runnable, long delayMs) { 201 return enqueueTask(new RunnableTask(runnable, SystemClock.elapsedRealtime() + delayMs), 202 delayMs); 203 } 204 205 /** 206 * Remove a scheduled runnable. 207 * 208 * @param runnable the runnable to be removed 209 */ removeDelayedRunnable(@onNull Runnable runnable)210 public void removeDelayedRunnable(@NonNull Runnable runnable) { 211 ensureRunningOnCorrectThread(); 212 mTaskQueue.removeIf(task -> task instanceof RunnableTask 213 && ((RunnableTask) task).mRunnable == runnable); 214 } 215 216 /** 217 * Set a message to be sent after a specified delay. 218 * 219 * If delayMs is less than or equal to 0, the message will be sent immediately. 220 * 221 * @param msg the message to be sent 222 * @param delayMs the delay time in milliseconds 223 * @return true if the message is scheduled successfully, false otherwise. 224 */ sendDelayedMessage(Message msg, long delayMs)225 public boolean sendDelayedMessage(Message msg, long delayMs) { 226 227 return enqueueTask(new MessageTask(msg, SystemClock.elapsedRealtime() + delayMs), delayMs); 228 } 229 isMessageTask(Task task, int what)230 private static boolean isMessageTask(Task task, int what) { 231 if (task instanceof MessageTask && ((MessageTask) task).mMessage.what == what) { 232 return true; 233 } 234 return false; 235 } 236 237 /** 238 * Remove a scheduled message. 239 * 240 * @param what the message to be removed 241 */ removeDelayedMessage(int what)242 public void removeDelayedMessage(int what) { 243 ensureRunningOnCorrectThread(); 244 mTaskQueue.removeIf(task -> isMessageTask(task, what)); 245 } 246 247 /** 248 * Check if there is a scheduled message. 249 * 250 * @param what the message to be checked 251 * @return true if there is a target message, false otherwise. 252 */ hasDelayedMessage(int what)253 public boolean hasDelayedMessage(int what) { 254 ensureRunningOnCorrectThread(); 255 256 for (Task task : mTaskQueue) { 257 if (isMessageTask(task, what)) { 258 return true; 259 } 260 } 261 return false; 262 } 263 264 /** 265 * Close the RealtimeScheduler. This implementation closes the underlying 266 * OS resources allocated to represent this stream. 267 */ close()268 public void close() { 269 ensureRunningOnCorrectThread(); 270 unregisterAndDestroyFd(); 271 } 272 registerFdEventListener()273 private void registerFdEventListener() { 274 mQueue.addOnFileDescriptorEventListener( 275 mParcelFileDescriptor.getFileDescriptor(), 276 FD_EVENTS, 277 (fd, events) -> { 278 if (!isRunning()) { 279 return 0; 280 } 281 if ((events & EVENT_ERROR) != 0) { 282 Log.wtf(TAG, "Got EVENT_ERROR from FileDescriptorEventListener."); 283 return 0; 284 } 285 if ((events & EVENT_INPUT) != 0) { 286 handleExpiration(); 287 } 288 return FD_EVENTS; 289 }); 290 } 291 isRunning()292 private boolean isRunning() { 293 return mParcelFileDescriptor.getFileDescriptor().valid(); 294 } 295 handleExpiration()296 private void handleExpiration() { 297 // The data from the FileDescriptor must be read after the timer expires. Otherwise, 298 // expiration callbacks will continue to be sent, notifying of unread data. The content(the 299 // number of expirations) can be ignored, as the callback is the only item of interest. 300 // Refer to https://man7.org/linux/man-pages/man2/timerfd_create.2.html 301 // read(2) 302 // If the timer has already expired one or more times since 303 // its settings were last modified using timerfd_settime(), 304 // or since the last successful read(2), then the buffer 305 // given to read(2) returns an unsigned 8-byte integer 306 // (uint64_t) containing the number of expirations that have 307 // occurred. (The returned value is in host byte order—that 308 // is, the native byte order for integers on the host 309 // machine.) 310 final byte[] readBuffer = new byte[8]; 311 try { 312 Os.read(mParcelFileDescriptor.getFileDescriptor(), readBuffer, 0, readBuffer.length); 313 } catch (IOException | ErrnoException exception) { 314 Log.wtf(TAG, "Read FileDescriptor failed. ", exception); 315 } 316 317 long currentTimeMs = SystemClock.elapsedRealtime(); 318 while (!mTaskQueue.isEmpty()) { 319 final Task task = mTaskQueue.peek(); 320 currentTimeMs = SystemClock.elapsedRealtime(); 321 if (currentTimeMs < task.getRunTimeMs()) { 322 break; 323 } 324 task.post(mHandler); 325 mTaskQueue.poll(); 326 } 327 328 if (!mTaskQueue.isEmpty()) { 329 // Using currentTimeMs ensures that the calculated expiration time 330 // is always positive. 331 if (!TimerFdUtils.setExpirationTime(mFdInt, 332 mTaskQueue.peek().getRunTimeMs() - currentTimeMs)) { 333 // If setting the expiration time fails, clear the task queue. 334 Log.wtf(TAG, "Failed to set expiration time"); 335 mTaskQueue.clear(); 336 } 337 } 338 } 339 unregisterAndDestroyFd()340 private void unregisterAndDestroyFd() { 341 if (mGuard != null) { 342 mGuard.close(); 343 } 344 345 mQueue.removeOnFileDescriptorEventListener(mParcelFileDescriptor.getFileDescriptor()); 346 try { 347 mParcelFileDescriptor.close(); 348 } catch (IOException exception) { 349 Log.e(TAG, "close ParcelFileDescriptor failed. ", exception); 350 } 351 } 352 ensureRunningOnCorrectThread()353 private void ensureRunningOnCorrectThread() { 354 if (mHandler.getLooper() != Looper.myLooper()) { 355 throw new IllegalStateException( 356 "Not running on Handler thread: " + Thread.currentThread().getName()); 357 } 358 } 359 360 @SuppressWarnings("Finalize") 361 @Override finalize()362 protected void finalize() throws Throwable { 363 if (mGuard != null) { 364 mGuard.warnIfOpen(); 365 } 366 super.finalize(); 367 } 368 } 369