Merge "Build the timerfd wrapper class" into main am: e8e32ab330 am: 9a470fc4e5
Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/3268573
Change-Id: Ia49d93427933393abf63b44eea59554f8440424c
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
index 310dbc9..c7ed911 100644
--- a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
+++ b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
@@ -26,12 +26,13 @@
*/
public class TimerFdUtils {
static {
- if (Process.myUid() == Process.SYSTEM_UID) {
+ final String jniLibName = JniUtil.getJniLibraryName(TimerFdUtils.class.getPackage());
+ if (jniLibName.equals("android_net_connectivity_com_android_net_module_util_jni")) {
// This library is part of service-connectivity.jar when in the system server,
// so libservice-connectivity.so is the library to load.
System.loadLibrary("service-connectivity");
} else {
- System.loadLibrary(JniUtil.getJniLibraryName(TimerFdUtils.class.getPackage()));
+ System.loadLibrary(jniLibName);
}
}
diff --git a/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java b/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java
new file mode 100644
index 0000000..5a16de6
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java
@@ -0,0 +1,199 @@
+/*
+ * 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.MessageQueue;
+import android.os.ParcelFileDescriptor;
+import android.util.CloseGuard;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.IOException;
+
+/**
+ * Represents a Timer file descriptor 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.
+ *
+ * <p> This class is designed for use exclusively from the handler thread.
+ *
+ * **Usage Examples:**
+ *
+ * ** Scheduling recurring tasks with the same TimerFileDescriptor **
+ *
+ * ```java
+ * // Create a TimerFileDescriptor
+ * final TimerFileDescriptor timerFd = new TimerFileDescriptor(handler);
+ *
+ * // Schedule a new task with a delay.
+ * timerFd.setDelayedTask(() -> taskToExecute(), delayTime);
+ *
+ * // Once the delay has elapsed, and the task is running, schedule another task.
+ * timerFd.setDelayedTask(() -> anotherTaskToExecute(), anotherDelayTime);
+ *
+ * // Remember to close the TimerFileDescriptor after all tasks have finished running.
+ * timerFd.close();
+ * ```
+ */
+public class TimerFileDescriptor {
+ private static final String TAG = TimerFileDescriptor.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;
+ @Nullable
+ private Runnable mTask;
+
+ /**
+ * TimerFileDescriptor 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 TimerFileDescriptor(@NonNull Handler handler) {
+ mFdInt = TimerFdUtils.createTimerFileDescriptor();
+ mParcelFileDescriptor = ParcelFileDescriptor.adoptFd(mFdInt);
+ mHandler = handler;
+ mQueue = handler.getLooper().getQueue();
+ registerFdEventListener();
+
+ mGuard.open("close");
+ }
+
+ /**
+ * Set a task to be executed after a specified delay.
+ *
+ * <p> A task can only be scheduled once at a time. Cancel previous scheduled task before the
+ * new task is scheduled.
+ *
+ * @param task the task to be executed
+ * @param delayMs the delay time in milliseconds
+ * @throws IllegalArgumentException if try to replace the current scheduled task
+ * @throws IllegalArgumentException if the delay time is less than 0
+ */
+ public void setDelayedTask(@NonNull Runnable task, long delayMs) {
+ ensureRunningOnCorrectThread();
+ if (mTask != null) {
+ throw new IllegalArgumentException("task is already scheduled");
+ }
+ if (delayMs <= 0L) {
+ mHandler.post(task);
+ return;
+ }
+
+ if (TimerFdUtils.setExpirationTime(mFdInt, delayMs)) {
+ mTask = task;
+ }
+ }
+
+ /**
+ * Cancel the scheduled task.
+ */
+ public void cancelTask() {
+ ensureRunningOnCorrectThread();
+ if (mTask == null) return;
+
+ TimerFdUtils.setExpirationTime(mFdInt, 0 /* delayMs */);
+ mTask = null;
+ }
+
+ /**
+ * Check if there is a scheduled task.
+ */
+ public boolean hasDelayedTask() {
+ ensureRunningOnCorrectThread();
+ return mTask != null;
+ }
+
+ /**
+ * Close the TimerFileDescriptor. 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_INPUT) != 0) {
+ handleExpiration();
+ }
+ return FD_EVENTS;
+ });
+ }
+
+ private boolean isRunning() {
+ return mParcelFileDescriptor.getFileDescriptor().valid();
+ }
+
+ private void handleExpiration() {
+ // Execute the task
+ if (mTask != null) {
+ mTask.run();
+ mTask = null;
+ }
+ }
+
+ 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();
+ }
+}
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 8c54e6a..9d1d291 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -55,6 +55,7 @@
// For mockito extended
"libdexmakerjvmtiagent",
"libstaticjvmtiagent",
+ "libcom_android_net_moduletests_util_jni",
],
jarjar_rules: "jarjar-rules.txt",
test_suites: ["device-tests"],
diff --git a/staticlibs/tests/unit/jni/Android.bp b/staticlibs/tests/unit/jni/Android.bp
new file mode 100644
index 0000000..e456471
--- /dev/null
+++ b/staticlibs/tests/unit/jni/Android.bp
@@ -0,0 +1,39 @@
+// 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 {
+ default_team: "trendy_team_fwk_core_networking",
+ // See: http://go/android-license-faq
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_shared {
+ name: "libcom_android_net_moduletests_util_jni",
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ "-Wthread-safety",
+ ],
+ srcs: [
+ "com_android_net_moduletests_util/onload.cpp",
+ ],
+ static_libs: [
+ "libnet_utils_device_common_timerfdjni",
+ ],
+ shared_libs: [
+ "liblog",
+ "libnativehelper",
+ ],
+}
diff --git a/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp b/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp
new file mode 100644
index 0000000..a035540
--- /dev/null
+++ b/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+#include "jni.h"
+#include <nativehelper/JNIHelp.h>
+
+#define LOG_TAG "NetworkStaticLibTestsJni"
+#include <android/log.h>
+
+namespace android {
+
+int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
+ char const *class_name);
+
+extern "C" jint JNI_OnLoad(JavaVM *vm, void *) {
+ JNIEnv *env;
+ if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, "ERROR: GetEnv failed");
+ return JNI_ERR;
+ }
+
+ if (register_com_android_net_module_util_TimerFdUtils(
+ env, "com/android/net/moduletests/util/TimerFdUtils") < 0)
+ return JNI_ERR;
+
+ return JNI_VERSION_1_6;
+}
+
+}; // namespace android
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/TimerFileDescriptorTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/TimerFileDescriptorTest.kt
new file mode 100644
index 0000000..2018902
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/TimerFileDescriptorTest.kt
@@ -0,0 +1,86 @@
+/*
+ * 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 android.os.Build
+import android.os.ConditionVariable
+import android.os.Handler
+import android.os.HandlerThread
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.tryTest
+import com.android.testutils.visibleOnHandlerThread
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.time.Duration
+import java.time.Instant
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class TimerFileDescriptorTest {
+ private val thread = HandlerThread(TimerFileDescriptorTest::class.simpleName).apply { start() }
+ private val handler by lazy { Handler(thread.looper) }
+
+ @After
+ fun tearDown() {
+ thread.quitSafely()
+ thread.join()
+ }
+
+ @Test
+ fun testSetDelayedTask() {
+ val delayTime = 10L
+ val timerFd = TimerFileDescriptor(handler)
+ val cv = ConditionVariable()
+ val startTime = Instant.now()
+ tryTest {
+ handler.post { timerFd.setDelayedTask({ cv.open() }, delayTime) }
+ assertTrue(cv.block(100L /* timeoutMs*/))
+ // Verify that the delay time has actually passed.
+ val duration = Duration.between(startTime, Instant.now())
+ assertTrue(duration.toMillis() >= delayTime)
+ } cleanup {
+ visibleOnHandlerThread(handler) { timerFd.close() }
+ }
+ }
+
+ @Test
+ fun testCancelTask() {
+ // The task is posted and canceled within the same handler loop, so the short delay used
+ // here won't cause flakes.
+ val delayTime = 10L
+ val timerFd = TimerFileDescriptor(handler)
+ val cv = ConditionVariable()
+ tryTest {
+ handler.post {
+ timerFd.setDelayedTask({ cv.open() }, delayTime)
+ assertTrue(timerFd.hasDelayedTask())
+ timerFd.cancelTask()
+ assertFalse(timerFd.hasDelayedTask())
+ }
+ assertFalse(cv.block(20L /* timeoutMs*/))
+ } cleanup {
+ visibleOnHandlerThread(handler) { timerFd.close() }
+ }
+ }
+}