1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.hardware.devicestate.cts;
18 
19 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
20 
21 import android.hardware.devicestate.DeviceStateManager;
22 import android.hardware.devicestate.DeviceStateRequest;
23 import android.server.wm.ActivityManagerTestBase;
24 
25 import androidx.annotation.CallSuper;
26 import androidx.annotation.NonNull;
27 import androidx.annotation.Nullable;
28 import androidx.test.ext.junit.runners.AndroidJUnit4;
29 
30 import org.junit.Before;
31 import org.junit.runner.RunWith;
32 
33 import java.util.concurrent.CountDownLatch;
34 import java.util.concurrent.TimeUnit;
35 
36 import javax.annotation.concurrent.GuardedBy;
37 
38 /**
39  * Abstract base class for {@link DeviceStateManager} CTS tests.
40  */
41 @RunWith(AndroidJUnit4.class)
42 public abstract class DeviceStateManagerTestBase extends ActivityManagerTestBase {
43     static final int CALLBACK_TIMEOUT_MS = 1000;
44 
45     private DeviceStateManager mDeviceStateManager;
46 
47     @CallSuper
48     @Before
setup()49     public void setup() {
50         mDeviceStateManager = getInstrumentation().getTargetContext()
51                 .getSystemService(DeviceStateManager.class);
52     }
53 
54     /** Returns an instance of {@link DeviceStateManager} for use in tests. */
55     @NonNull
getDeviceStateManager()56     DeviceStateManager getDeviceStateManager() {
57         if (mDeviceStateManager == null) {
58             // called before setup();
59             throw new IllegalStateException();
60         }
61         return mDeviceStateManager;
62     }
63 
64     /**
65      * Runs the supplied {@code Runnable} ensuring the {@code request} is active during execution.
66      * If the request becomes suspended or canceled before or during runnable execution a
67      * {@link java.lang.InterruptedException} will be thrown.
68      */
runWithRequestActive(@onNull DeviceStateRequest request, boolean isBaseStateRequest, @NonNull Runnable runnable)69     protected final void runWithRequestActive(@NonNull DeviceStateRequest request,
70             boolean isBaseStateRequest,
71             @NonNull Runnable runnable) throws Throwable {
72         final UncaughtExceptionHandler exceptionHandler = new UncaughtExceptionHandler();
73         final RequestAwareThread thread = new RequestAwareThread(request, runnable);
74         thread.setUncaughtExceptionHandler(exceptionHandler);
75         try (DeviceStateRequestSession session =
76                      new DeviceStateRequestSession(mDeviceStateManager, request,
77                 isBaseStateRequest, thread)) {
78             // Set the exception handler to get the exception and rethrow.
79             thread.start();
80             // Wait for the request aware thread to finish executing the runnable. If the request
81             // is suspended or canceled this method will throw an InterruptedException.
82             thread.join();
83         }
84 
85         // Rethrow any exceptions from the runnable.
86         final Throwable t = exceptionHandler.getThrowable();
87         if (t != null) {
88             throw t;
89         }
90     }
91 
92     /**
93      * An implementation of {@link Thread} that listens to changes in a request state and
94      * automatically interrupts if the request is suspended or canceled while the thread
95      * is running.
96      */
97     private static final class RequestAwareThread extends Thread
98             implements DeviceStateRequest.Callback {
99         private final Object mLock = new Object();
100 
101         private final CountDownLatch mActiveLatch = new CountDownLatch(1);
102 
103         @NonNull
104         private final DeviceStateRequest mRequest;
105         @NonNull
106         private final Runnable mRunnable;
107 
108         @GuardedBy("mLock")
109         private boolean mIsRunning;
110         @GuardedBy("mLock")
111         private boolean mWasSuspendedOrCanceled;
112 
RequestAwareThread(@onNull DeviceStateRequest request, @NonNull Runnable runnable)113         private RequestAwareThread(@NonNull DeviceStateRequest request,
114                 @NonNull Runnable runnable) {
115             mRequest = request;
116             mRunnable = runnable;
117         }
118 
119         @Override
run()120         public void run() {
121             // Wait for the request to be active.
122             boolean success;
123             try {
124                 success = mActiveLatch.await(CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
125             } catch (InterruptedException e) {
126                 // This thread was interrupted while waiting for the callback.
127                 success = false;
128             }
129             if (!success) {
130                 throw new RuntimeException("Timed out waiting for " + toString(mRequest)
131                         + " to become active.");
132             }
133             synchronized (mLock) {
134                 if (mWasSuspendedOrCanceled) {
135                     interrupt();
136                     return;
137                 }
138                 mIsRunning = true;
139             }
140             try {
141                 mRunnable.run();
142             } finally {
143                 synchronized (mLock) {
144                     mIsRunning = false;
145                 }
146             }
147         }
148 
149         @Override
onRequestActivated(@onNull DeviceStateRequest request)150         public void onRequestActivated(@NonNull DeviceStateRequest request) {
151             if (!request.equals(mRequest)) {
152                 return;
153             }
154 
155             mActiveLatch.countDown();
156         }
157 
158         @Override
onRequestSuspended(@onNull DeviceStateRequest request)159         public void onRequestSuspended(@NonNull DeviceStateRequest request) {
160             if (!request.equals(mRequest)) {
161                 return;
162             }
163 
164             synchronized (mLock) {
165                 mWasSuspendedOrCanceled = true;
166                 interruptIfRunningLocked();
167             }
168         }
169 
170         @Override
onRequestCanceled(@onNull DeviceStateRequest request)171         public void onRequestCanceled(@NonNull DeviceStateRequest request) {
172             if (!request.equals(mRequest)) {
173                 return;
174             }
175 
176             synchronized (mLock) {
177                 mWasSuspendedOrCanceled = true;
178                 interruptIfRunningLocked();
179             }
180         }
181 
interruptIfRunningLocked()182         private void interruptIfRunningLocked() {
183             if (mIsRunning) {
184                 // Interrupt this thread if the runnable is still running and the request was
185                 // cancelled or suspended.
186                 interrupt();
187             }
188         }
189 
toString(@onNull DeviceStateRequest request)190         private static String toString(@NonNull DeviceStateRequest request) {
191             return "DeviceStateRequest{state=" + request.getState() + ", flags="
192                     + request.getFlags() + "}";
193         }
194     }
195 
196     /**
197      * An implementation of {@link Thread.UncaughtExceptionHandler} that simply stores the latest
198      * notified uncaught exception.
199      */
200     private static final class UncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
201         @Nullable
202         private Throwable mThrowable;
203 
204         @Override
uncaughtException(Thread t, Throwable e)205         public void uncaughtException(Thread t, Throwable e) {
206             mThrowable = e;
207         }
208 
209         @Nullable
getThrowable()210         public Throwable getThrowable() {
211             return mThrowable;
212         }
213     }
214 }
215