• 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.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