blob: 7e5a768ab04eb5a39560d5b00c896697a7597645 [file] [log] [blame]
/*
* 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;
}
}
}