| /* |
| * 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 android.hardware.devicestate.cts; |
| |
| import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; |
| |
| import android.hardware.devicestate.DeviceStateManager; |
| import android.hardware.devicestate.DeviceStateRequest; |
| import android.server.wm.ActivityManagerTestBase; |
| |
| import androidx.annotation.CallSuper; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.test.ext.junit.runners.AndroidJUnit4; |
| |
| import org.junit.Before; |
| import org.junit.runner.RunWith; |
| |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| |
| import javax.annotation.concurrent.GuardedBy; |
| |
| /** |
| * Abstract base class for {@link DeviceStateManager} CTS tests. |
| */ |
| @RunWith(AndroidJUnit4.class) |
| public abstract class DeviceStateManagerTestBase extends ActivityManagerTestBase { |
| static final int CALLBACK_TIMEOUT_MS = 1000; |
| |
| private DeviceStateManager mDeviceStateManager; |
| |
| @CallSuper |
| @Before |
| public void setup() { |
| mDeviceStateManager = getInstrumentation().getTargetContext() |
| .getSystemService(DeviceStateManager.class); |
| } |
| |
| /** Returns an instance of {@link DeviceStateManager} for use in tests. */ |
| @NonNull |
| DeviceStateManager getDeviceStateManager() { |
| if (mDeviceStateManager == null) { |
| // called before setup(); |
| throw new IllegalStateException(); |
| } |
| return mDeviceStateManager; |
| } |
| |
| /** |
| * Runs the supplied {@code Runnable} ensuring the {@code request} is active during execution. |
| * If the request becomes suspended or canceled before or during runnable execution a |
| * {@link java.lang.InterruptedException} will be thrown. |
| */ |
| protected final void runWithRequestActive(@NonNull DeviceStateRequest request, |
| @NonNull Runnable runnable) throws Throwable { |
| final UncaughtExceptionHandler exceptionHandler = new UncaughtExceptionHandler(); |
| final RequestAwareThread thread = new RequestAwareThread(request, runnable); |
| thread.setUncaughtExceptionHandler(exceptionHandler); |
| try (DeviceStateRequestSession session |
| = new DeviceStateRequestSession(mDeviceStateManager, request, thread)) { |
| // Set the exception handler to get the exception and rethrow. |
| thread.start(); |
| // Wait for the request aware thread to finish executing the runnable. If the request |
| // is suspended or canceled this method will throw an InterruptedException. |
| thread.join(); |
| } |
| |
| // Rethrow any exceptions from the runnable. |
| final Throwable t = exceptionHandler.getThrowable(); |
| if (t != null) { |
| throw t; |
| } |
| } |
| |
| /** |
| * An implementation of {@link Thread} that listens to changes in a request state and |
| * automatically interrupts if the request is suspended or canceled while the thread |
| * is running. |
| */ |
| private static final class RequestAwareThread extends Thread |
| implements DeviceStateRequest.Callback { |
| private final Object mLock = new Object(); |
| |
| private final CountDownLatch mActiveLatch = new CountDownLatch(1); |
| |
| @NonNull |
| private final DeviceStateRequest mRequest; |
| @NonNull |
| private final Runnable mRunnable; |
| |
| @GuardedBy("mLock") |
| private boolean mIsRunning; |
| @GuardedBy("mLock") |
| private boolean mWasSuspendedOrCanceled; |
| |
| private RequestAwareThread(@NonNull DeviceStateRequest request, |
| @NonNull Runnable runnable) { |
| mRequest = request; |
| mRunnable = runnable; |
| } |
| |
| @Override |
| public void run() { |
| // Wait for the request to be active. |
| boolean success; |
| try { |
| success = mActiveLatch.await(CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
| } catch (InterruptedException e) { |
| // This thread was interrupted while waiting for the callback. |
| success = false; |
| } |
| if (!success) { |
| throw new RuntimeException("Timed out waiting for " + toString(mRequest) |
| + " to become active."); |
| } |
| synchronized (mLock) { |
| if (mWasSuspendedOrCanceled) { |
| interrupt(); |
| return; |
| } |
| mIsRunning = true; |
| } |
| try { |
| mRunnable.run(); |
| } finally { |
| synchronized (mLock) { |
| mIsRunning = false; |
| } |
| } |
| } |
| |
| @Override |
| public void onRequestActivated(@NonNull DeviceStateRequest request) { |
| if (!request.equals(mRequest)) { |
| return; |
| } |
| |
| mActiveLatch.countDown(); |
| } |
| |
| @Override |
| public void onRequestSuspended(@NonNull DeviceStateRequest request) { |
| if (!request.equals(mRequest)) { |
| return; |
| } |
| |
| synchronized (mLock) { |
| mWasSuspendedOrCanceled = true; |
| interruptIfRunningLocked(); |
| } |
| } |
| |
| @Override |
| public void onRequestCanceled(@NonNull DeviceStateRequest request) { |
| if (!request.equals(mRequest)) { |
| return; |
| } |
| |
| synchronized (mLock) { |
| mWasSuspendedOrCanceled = true; |
| interruptIfRunningLocked(); |
| } |
| } |
| |
| private void interruptIfRunningLocked() { |
| if (mIsRunning) { |
| // Interrupt this thread if the runnable is still running and the request was |
| // cancelled or suspended. |
| interrupt(); |
| } |
| } |
| |
| private static String toString(@NonNull DeviceStateRequest request) { |
| return "DeviceStateRequest{state=" + request.getState() + ", flags=" |
| + request.getFlags() + "}"; |
| } |
| } |
| |
| /** |
| * An implementation of {@link Thread.UncaughtExceptionHandler} that simply stores the latest |
| * notified uncaught exception. |
| */ |
| private static final class UncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { |
| @Nullable |
| private Throwable mThrowable; |
| |
| @Override |
| public void uncaughtException(Thread t, Throwable e) { |
| mThrowable = e; |
| } |
| |
| @Nullable |
| public Throwable getThrowable() { |
| return mThrowable; |
| } |
| } |
| } |