diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHalWatchdog.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHalWatchdog.java index 5fe06eef146d2..227424ea1780f 100644 --- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHalWatchdog.java +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHalWatchdog.java @@ -26,8 +26,6 @@ import android.os.IBinder; import android.util.Log; import java.util.Objects; -import java.util.Timer; -import java.util.TimerTask; /** * An {@link ISoundTriggerHal} decorator that would enforce deadlines on all calls and reboot the @@ -38,11 +36,11 @@ public class SoundTriggerHalWatchdog implements ISoundTriggerHal { private static final String TAG = "SoundTriggerHalWatchdog"; private final @NonNull ISoundTriggerHal mUnderlying; - private final @NonNull Timer mTimer; + private final @NonNull UptimeTimer mTimer; public SoundTriggerHalWatchdog(@NonNull ISoundTriggerHal underlying) { mUnderlying = Objects.requireNonNull(underlying); - mTimer = new Timer("SoundTriggerHalWatchdog"); + mTimer = new UptimeTimer("SoundTriggerHalWatchdog"); } @Override @@ -155,21 +153,16 @@ public class SoundTriggerHalWatchdog implements ISoundTriggerHal { } private class Watchdog implements AutoCloseable { - private final @NonNull - TimerTask mTask; + private final @NonNull UptimeTimer.Task mTask; // This exception is used merely for capturing a stack trace at the time of creation. private final @NonNull Exception mException = new Exception(); Watchdog() { - mTask = new TimerTask() { - @Override - public void run() { - Log.e(TAG, "HAL deadline expired. Rebooting.", mException); - reboot(); - } - }; - mTimer.schedule(mTask, TIMEOUT_MS); + mTask = mTimer.createTask(() -> { + Log.e(TAG, "HAL deadline expired. Rebooting.", mException); + reboot(); + }, TIMEOUT_MS); } @Override diff --git a/services/core/java/com/android/server/soundtrigger_middleware/UptimeTimer.java b/services/core/java/com/android/server/soundtrigger_middleware/UptimeTimer.java new file mode 100644 index 0000000000000..bfcc7d840662e --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/UptimeTimer.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2021 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.server.soundtrigger_middleware; + +import android.annotation.NonNull; +import android.os.Handler; +import android.os.Looper; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * A simple timer, similar to java.util.Timer, but using the "uptime clock". + * + * Example usage: + * UptimeTimer timer = new UptimeTimer("TimerThread"); + * UptimeTimer.Task task = timer.createTask(() -> { ... }, 100); + * ... + * // optionally, some time later: + * task.cancel(); + */ +class UptimeTimer { + private Handler mHandler = null; + + interface Task { + void cancel(); + } + + UptimeTimer(String threadName) { + new Thread(this::threadFunc, threadName).start(); + synchronized (this) { + while (mHandler == null) { + try { + wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + } + + Task createTask(@NonNull Runnable runnable, long uptimeMs) { + TaskImpl task = new TaskImpl(runnable); + mHandler.postDelayed(task, uptimeMs); + return task; + } + + private void threadFunc() { + Looper.prepare(); + synchronized (this) { + mHandler = new Handler(Looper.myLooper()); + notifyAll(); + } + Looper.loop(); + } + + private static class TaskImpl implements Task, Runnable { + private AtomicReference mRunnable = new AtomicReference<>(); + + TaskImpl(@NonNull Runnable runnable) { + mRunnable.set(runnable); + } + + @Override + public void cancel() { + mRunnable.set(null); + } + + @Override + public void run() { + Runnable runnable = mRunnable.get(); + if (runnable != null) { + runnable.run(); + } + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/UptimeTimerTest.java b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/UptimeTimerTest.java new file mode 100644 index 0000000000000..38297bf624b7d --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/UptimeTimerTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021 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.server.soundtrigger_middleware; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.atomic.AtomicBoolean; + +@RunWith(AndroidJUnit4.class) +public class UptimeTimerTest { + private static final String TAG = "UptimeTimerTest"; + + @Test + public void testBasic() throws InterruptedException { + AtomicBoolean taskRan = new AtomicBoolean(false); + UptimeTimer timer = new UptimeTimer("TestTimer"); + timer.createTask(() -> taskRan.set(true), 100); + Thread.sleep(50); + boolean before = taskRan.get(); + Thread.sleep(100); + boolean after = taskRan.get(); + assertFalse(before); + assertTrue(after); + } + + @Test + public void testCancel() throws InterruptedException { + AtomicBoolean taskRan = new AtomicBoolean(false); + UptimeTimer timer = new UptimeTimer("TestTimer"); + UptimeTimer.Task task = timer.createTask(() -> taskRan.set(true), 100); + Thread.sleep(50); + boolean before = taskRan.get(); + task.cancel(); + Thread.sleep(100); + boolean after = taskRan.get(); + assertFalse(before); + assertFalse(after); + } +}