/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.net.module.util;
import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.system.ErrnoException;
import android.system.Os;
import android.util.CloseGuard;
import android.util.Log;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.util.PriorityQueue;
/**
* Represents a realtime scheduler object used for scheduling tasks with precise delays.
* Compared to {@link Handler#postDelayed}, this class offers enhanced accuracy for delayed
* callbacks by accounting for periods when the device is in deep sleep.
*
*
This class is designed for use exclusively from the handler thread.
*
* **Usage Examples:**
*
* ** Scheduling recurring tasks with the same RealtimeScheduler **
*
* ```java
* // Create a RealtimeScheduler
* final RealtimeScheduler scheduler = new RealtimeScheduler(handler);
*
* // Schedule a new task with a delay.
* scheduler.postDelayed(() -> taskToExecute(), delayTime);
*
* // Once the delay has elapsed, and the task is running, schedule another task.
* scheduler.postDelayed(() -> anotherTaskToExecute(), anotherDelayTime);
*
* // Remember to close the RealtimeScheduler after all tasks have finished running.
* scheduler.close();
* ```
*/
public class RealtimeScheduler {
private static final String TAG = RealtimeScheduler.class.getSimpleName();
// EVENT_ERROR may be generated even if not specified, as per its javadoc.
private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
private final CloseGuard mGuard = new CloseGuard();
@NonNull
private final Handler mHandler;
@NonNull
private final MessageQueue mQueue;
@NonNull
private final ParcelFileDescriptor mParcelFileDescriptor;
private final int mFdInt;
private final PriorityQueue mTaskQueue;
/**
* An abstract class for defining tasks that can be executed using a {@link Handler}.
*/
private abstract static class Task implements Comparable {
private final long mRunTimeMs;
private final long mCreatedTimeNs = SystemClock.elapsedRealtimeNanos();
/**
* create a task with a run time
*/
Task(long runTimeMs) {
mRunTimeMs = runTimeMs;
}
/**
* Executes the task using the provided {@link Handler}.
*
* @param handler The {@link Handler} to use for executing the task.
*/
abstract void post(Handler handler);
@Override
public int compareTo(@NonNull Task o) {
if (mRunTimeMs != o.mRunTimeMs) {
return Long.compare(mRunTimeMs, o.mRunTimeMs);
}
return Long.compare(mCreatedTimeNs, o.mCreatedTimeNs);
}
/**
* Returns the run time of the task.
*/
public long getRunTimeMs() {
return mRunTimeMs;
}
}
/**
* A task that sends a {@link Message} using a {@link Handler}.
*/
private static class MessageTask extends Task {
private final Message mMessage;
MessageTask(Message message, long runTimeMs) {
super(runTimeMs);
mMessage = message;
}
/**
* Sends the {@link Message} using the provided {@link Handler}.
*
* @param handler The {@link Handler} to use for sending the message.
*/
@Override
public void post(Handler handler) {
handler.sendMessage(mMessage);
}
}
/**
* A task that posts a {@link Runnable} to a {@link Handler}.
*/
private static class RunnableTask extends Task {
private final Runnable mRunnable;
RunnableTask(Runnable runnable, long runTimeMs) {
super(runTimeMs);
mRunnable = runnable;
}
/**
* Posts the {@link Runnable} to the provided {@link Handler}.
*
* @param handler The {@link Handler} to use for posting the runnable.
*/
@Override
public void post(Handler handler) {
handler.post(mRunnable);
}
}
/**
* The RealtimeScheduler constructor
*
* Note: The constructor is currently safe to call on another thread because it only sets final
* members and registers the event to be called on the handler.
*/
public RealtimeScheduler(@NonNull Handler handler) {
mFdInt = TimerFdUtils.createTimerFileDescriptor();
mParcelFileDescriptor = ParcelFileDescriptor.adoptFd(mFdInt);
mHandler = handler;
mQueue = handler.getLooper().getQueue();
mTaskQueue = new PriorityQueue<>();
registerFdEventListener();
mGuard.open("close");
}
private boolean enqueueTask(@NonNull Task task, long delayMs) {
ensureRunningOnCorrectThread();
if (delayMs <= 0L) {
task.post(mHandler);
return true;
}
if (mTaskQueue.isEmpty() || task.compareTo(mTaskQueue.peek()) < 0) {
if (!TimerFdUtils.setExpirationTime(mFdInt, delayMs)) {
return false;
}
}
mTaskQueue.add(task);
return true;
}
/**
* Set a runnable to be executed after a specified delay.
*
* If delayMs is less than or equal to 0, the runnable will be executed immediately.
*
* @param runnable the runnable to be executed
* @param delayMs the delay time in milliseconds
* @return true if the task is scheduled successfully, false otherwise.
*/
public boolean postDelayed(@NonNull Runnable runnable, long delayMs) {
return enqueueTask(new RunnableTask(runnable, SystemClock.elapsedRealtime() + delayMs),
delayMs);
}
/**
* Remove a scheduled runnable.
*
* @param runnable the runnable to be removed
*/
public void removeDelayedRunnable(@NonNull Runnable runnable) {
ensureRunningOnCorrectThread();
mTaskQueue.removeIf(task -> task instanceof RunnableTask
&& ((RunnableTask) task).mRunnable == runnable);
}
/**
* Set a message to be sent after a specified delay.
*
* If delayMs is less than or equal to 0, the message will be sent immediately.
*
* @param msg the message to be sent
* @param delayMs the delay time in milliseconds
* @return true if the message is scheduled successfully, false otherwise.
*/
public boolean sendDelayedMessage(Message msg, long delayMs) {
return enqueueTask(new MessageTask(msg, SystemClock.elapsedRealtime() + delayMs), delayMs);
}
private static boolean isMessageTask(Task task, int what) {
if (task instanceof MessageTask && ((MessageTask) task).mMessage.what == what) {
return true;
}
return false;
}
/**
* Remove a scheduled message.
*
* @param what the message to be removed
*/
public void removeDelayedMessage(int what) {
ensureRunningOnCorrectThread();
mTaskQueue.removeIf(task -> isMessageTask(task, what));
}
/**
* Check if there is a scheduled message.
*
* @param what the message to be checked
* @return true if there is a target message, false otherwise.
*/
public boolean hasDelayedMessage(int what) {
ensureRunningOnCorrectThread();
for (Task task : mTaskQueue) {
if (isMessageTask(task, what)) {
return true;
}
}
return false;
}
/**
* Close the RealtimeScheduler. This implementation closes the underlying
* OS resources allocated to represent this stream.
*/
public void close() {
ensureRunningOnCorrectThread();
unregisterAndDestroyFd();
}
private void registerFdEventListener() {
mQueue.addOnFileDescriptorEventListener(
mParcelFileDescriptor.getFileDescriptor(),
FD_EVENTS,
(fd, events) -> {
if (!isRunning()) {
return 0;
}
if ((events & EVENT_ERROR) != 0) {
Log.wtf(TAG, "Got EVENT_ERROR from FileDescriptorEventListener.");
return 0;
}
if ((events & EVENT_INPUT) != 0) {
handleExpiration();
}
return FD_EVENTS;
});
}
private boolean isRunning() {
return mParcelFileDescriptor.getFileDescriptor().valid();
}
private void handleExpiration() {
// The data from the FileDescriptor must be read after the timer expires. Otherwise,
// expiration callbacks will continue to be sent, notifying of unread data. The content(the
// number of expirations) can be ignored, as the callback is the only item of interest.
// Refer to https://man7.org/linux/man-pages/man2/timerfd_create.2.html
// read(2)
// If the timer has already expired one or more times since
// its settings were last modified using timerfd_settime(),
// or since the last successful read(2), then the buffer
// given to read(2) returns an unsigned 8-byte integer
// (uint64_t) containing the number of expirations that have
// occurred. (The returned value is in host byte order—that
// is, the native byte order for integers on the host
// machine.)
final byte[] readBuffer = new byte[8];
try {
Os.read(mParcelFileDescriptor.getFileDescriptor(), readBuffer, 0, readBuffer.length);
} catch (IOException | ErrnoException exception) {
Log.wtf(TAG, "Read FileDescriptor failed. ", exception);
}
long currentTimeMs = SystemClock.elapsedRealtime();
while (!mTaskQueue.isEmpty()) {
final Task task = mTaskQueue.peek();
currentTimeMs = SystemClock.elapsedRealtime();
if (currentTimeMs < task.getRunTimeMs()) {
break;
}
task.post(mHandler);
mTaskQueue.poll();
}
if (!mTaskQueue.isEmpty()) {
// Using currentTimeMs ensures that the calculated expiration time
// is always positive.
if (!TimerFdUtils.setExpirationTime(mFdInt,
mTaskQueue.peek().getRunTimeMs() - currentTimeMs)) {
// If setting the expiration time fails, clear the task queue.
Log.wtf(TAG, "Failed to set expiration time");
mTaskQueue.clear();
}
}
}
private void unregisterAndDestroyFd() {
if (mGuard != null) {
mGuard.close();
}
mQueue.removeOnFileDescriptorEventListener(mParcelFileDescriptor.getFileDescriptor());
try {
mParcelFileDescriptor.close();
} catch (IOException exception) {
Log.e(TAG, "close ParcelFileDescriptor failed. ", exception);
}
}
private void ensureRunningOnCorrectThread() {
if (mHandler.getLooper() != Looper.myLooper()) {
throw new IllegalStateException(
"Not running on Handler thread: " + Thread.currentThread().getName());
}
}
@SuppressWarnings("Finalize")
@Override
protected void finalize() throws Throwable {
if (mGuard != null) {
mGuard.warnIfOpen();
}
super.finalize();
}
}