1 /*
2  * Copyright (C) 2022 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.app.sdksandbox.testutils.testscenario;
18 
19 import static android.app.sdksandbox.SdkSandboxManager.EXTRA_DISPLAY_ID;
20 import static android.app.sdksandbox.SdkSandboxManager.EXTRA_HEIGHT_IN_PIXELS;
21 import static android.app.sdksandbox.SdkSandboxManager.EXTRA_HOST_TOKEN;
22 import static android.app.sdksandbox.SdkSandboxManager.EXTRA_WIDTH_IN_PIXELS;
23 
24 import static com.google.common.truth.Truth.assertThat;
25 import static com.google.common.truth.Truth.assertWithMessage;
26 
27 import android.app.sdksandbox.RequestSurfacePackageException;
28 import android.app.sdksandbox.SandboxedSdk;
29 import android.app.sdksandbox.SdkSandboxManager;
30 import android.app.sdksandbox.testutils.FakeLoadSdkCallback;
31 import android.app.sdksandbox.testutils.FakeRequestSurfacePackageCallback;
32 import android.app.sdksandbox.testutils.SdkLifecycleHelper;
33 import android.app.sdksandbox.testutils.WaitableCountDownLatch;
34 import android.content.Context;
35 import android.os.Bundle;
36 import android.os.IBinder;
37 import android.text.TextUtils;
38 import android.view.SurfaceView;
39 import android.view.View;
40 
41 import androidx.annotation.IntDef;
42 import androidx.annotation.Nullable;
43 import androidx.lifecycle.Lifecycle;
44 import androidx.test.core.app.ActivityScenario;
45 import androidx.test.platform.app.InstrumentationRegistry;
46 
47 import org.junit.After;
48 import org.junit.Assert;
49 import org.junit.Assume;
50 import org.junit.Before;
51 import org.junit.rules.TestRule;
52 import org.junit.runner.Description;
53 import org.junit.runners.model.Statement;
54 
55 import java.lang.annotation.Retention;
56 import java.lang.annotation.RetentionPolicy;
57 import java.util.List;
58 import java.util.concurrent.atomic.AtomicReference;
59 
60 /**
61  * This rule is used to invoke tests inside SDKs. It loads a given Sdk, calls for a test to be
62  * executed inside given Sdk and unloads the Sdk once the execution is finished. When used as
63  * a @ClassRule, this rule will cause the sdksandbox to persist across tests by loading the sdk in
64  * the beginning of the class and unloading at the end. assertSdkTestRunPasses() contains the logic
65  * to trigger an in-SDK test and retrieve its results, while {@link SdkSandboxTestScenarioRunner}
66  * handles the Sdk-side logic for test execution.
67  */
68 public class SdkSandboxScenarioRule implements TestRule {
69     // This flag is used internally for behaviors that are
70     // enabled by default.
71     private static final int ENABLE_ALWAYS = 0x1;
72     // Execute "Before" and "After" annotations around tests.
73     public static final int ENABLE_LIFE_CYCLE_ANNOTATIONS = 0x2;
74 
75     // Use flags when you want to make a new behavior available
76     // to preserve backwards compatibility.
77     @IntDef({ENABLE_ALWAYS, ENABLE_LIFE_CYCLE_ANNOTATIONS})
78     @Retention(RetentionPolicy.SOURCE)
79     public @interface RuleOptions {}
80 
81     // We need to allow a fair amount of time to time out since we might
82     // want to execute fairly large tests.
83     private static final int TEST_TIMEOUT_S = 60;
84     private final String mSdkName;
85     private ISdkSandboxTestExecutor mTestExecutor;
86     private final Bundle mTestInstanceSetupParams;
87     private @Nullable IBinder mBinder;
88     private final int mFlags;
89     private SdkSandboxManager mSdkSandboxManager;
90     private SdkLifecycleHelper mSdkLifecycleHelper;
91 
SdkSandboxScenarioRule(String sdkName)92     public SdkSandboxScenarioRule(String sdkName) {
93         this(sdkName, null, null, ENABLE_ALWAYS);
94     }
95 
SdkSandboxScenarioRule( String sdkName, Bundle testInstanceSetupParams, @Nullable IBinder customInterface)96     public SdkSandboxScenarioRule(
97             String sdkName, Bundle testInstanceSetupParams, @Nullable IBinder customInterface) {
98         this(sdkName, testInstanceSetupParams, customInterface, ENABLE_ALWAYS);
99     }
100 
SdkSandboxScenarioRule( String sdkName, Bundle testInstanceSetupParams, @Nullable IBinder customInterface, @RuleOptions int flags)101     public SdkSandboxScenarioRule(
102             String sdkName,
103             Bundle testInstanceSetupParams,
104             @Nullable IBinder customInterface,
105             @RuleOptions int flags) {
106         mSdkName = sdkName;
107         mTestInstanceSetupParams = testInstanceSetupParams;
108         mBinder = customInterface;
109         // The always enable flag is added to the flags
110         // so that we have a way to indicate when a behavior
111         // is always enabled by default.
112         mFlags = flags | ENABLE_ALWAYS;
113     }
114 
115     @Override
apply(final Statement base, final Description description)116     public Statement apply(final Statement base, final Description description) {
117         return new Statement() {
118             @Override
119             public void evaluate() throws Throwable {
120                 try (ActivityScenario scenario =
121                         ActivityScenario.launch(SdkSandboxCtsActivity.class)) {
122                     final Context context =
123                             InstrumentationRegistry.getInstrumentation().getContext();
124                     mSdkSandboxManager = context.getSystemService(SdkSandboxManager.class);
125                     mSdkLifecycleHelper = new SdkLifecycleHelper(context);
126                     final IBinder textExecutor = loadTestSdk();
127                     if (textExecutor != null) {
128                         mTestExecutor = ISdkSandboxTestExecutor.Stub.asInterface(textExecutor);
129                     }
130                 }
131                 try {
132                     base.evaluate();
133                 } finally {
134                     try (ActivityScenario scenario =
135                             ActivityScenario.launch(SdkSandboxCtsActivity.class)) {
136                         mSdkLifecycleHelper.unloadSdk(mSdkName);
137                     }
138                 }
139             }
140         };
141     }
142 
143     public void assertSdkTestRunPasses(String testMethodName) throws Throwable {
144         assertSdkTestRunPasses(testMethodName, new Bundle());
145     }
146 
147     public void assertSdkTestRunPasses(String testMethodName, Bundle params) throws Throwable {
148         try (ActivityScenario scenario = ActivityScenario.launch(SdkSandboxCtsActivity.class)) {
149             if (mSdkSandboxManager.getSandboxedSdks().isEmpty()) {
150                 final IBinder textExecutor = loadTestSdk();
151                 if (textExecutor != null) {
152                     mTestExecutor = ISdkSandboxTestExecutor.Stub.asInterface(textExecutor);
153                 }
154             }
155             Assume.assumeTrue("No SDK executor. SDK sandbox is disabled", mTestExecutor != null);
156 
157             assertThat(scenario.getState()).isEqualTo(Lifecycle.State.RESUMED);
158             setView(scenario);
159 
160             Throwable testFailure = runBeforeTestMethods();
161 
162             if (testFailure == null) {
163                 runSdkMethod(testMethodName, params);
164             }
165 
166             // Even if "before methods" or tests fail, we are still expected to
167             // run "after methods" for clean up.
168             Throwable afterFailure = runAfterTestMethods();
169 
170             mTestExecutor.cleanOnTestFinish();
171 
172             if (testFailure != null) {
173                 throw testFailure;
174             } else if (afterFailure != null) {
175                 throw afterFailure;
176             }
177         }
178     }
179 
180     private Throwable runBeforeTestMethods() {
181         return tryDoWhen(
182                 ENABLE_LIFE_CYCLE_ANNOTATIONS,
183                 () -> {
184                     final List<String> beforeMethods =
185                             mTestExecutor.retrieveAnnotatedMethods(Before.class.getCanonicalName());
186                     for (final String before : beforeMethods) {
187                         runSdkMethod(before, new Bundle());
188                     }
189                 });
190     }
191 
192     private Throwable runAfterTestMethods() {
193         return tryDoWhen(
194                 ENABLE_LIFE_CYCLE_ANNOTATIONS,
195                 () -> {
196                     final List<String> afterMethods =
197                             mTestExecutor.retrieveAnnotatedMethods(After.class.getCanonicalName());
198                     for (final String after : afterMethods) {
199                         runSdkMethod(after, new Bundle());
200                     }
201                 });
202     }
203 
204     private void runSdkMethod(String methodName, Bundle params) throws Exception {
205         WaitableCountDownLatch testDoneLatch = new WaitableCountDownLatch(TEST_TIMEOUT_S);
206         AtomicReference<String> errorRef = new AtomicReference<>(null);
207 
208         ISdkSandboxResultCallback.Stub callback =
209                 new ISdkSandboxResultCallback.Stub() {
210                     public void onResult() {
211                         testDoneLatch.countDown();
212                     }
213 
214                     public void onError(String errorMessage) {
215                         if (TextUtils.isEmpty(errorMessage)) {
216                             errorRef.set(
217                                     String.format(
218                                             "Error executing method %s in sdk: Sdk returned no"
219                                                     + " stacktrace",
220                                             methodName));
221                         } else {
222                             errorRef.set(errorMessage);
223                         }
224                         testDoneLatch.countDown();
225                     }
226                 };
227 
228         assertThat(mTestExecutor).isNotNull();
229         mTestExecutor.invokeMethod(methodName, params, callback);
230 
231         testDoneLatch.waitForLatch("Sdk did not return any response");
232 
233         if (errorRef.get() != null) assertWithMessage(errorRef.get()).fail();
234         assertThat(true).isTrue();
235     }
236 
237     /**
238      * Will attempt to perform a {@link MightThrow} and return a throwable if it failed. It also
239      * expects a flag to gate if the behavior should be attempted or if it should just return.
240      */
241     private Throwable tryDoWhen(@RuleOptions int doWhenFlag, MightThrow mightThrow) {
242         try {
243             if (isFlagSet(doWhenFlag)) {
244                 mightThrow.call();
245             }
246             return null;
247         } catch (Throwable e) {
248             return e;
249         }
250     }
251 
252     // Returns test SDK for use in tests, returns null only if SDK sandbox is disabled.
253     private IBinder loadTestSdk() throws Exception {
254         final Bundle loadParams = new Bundle(2);
255         loadParams.putBundle(ISdkSandboxTestExecutor.TEST_SETUP_PARAMS, mTestInstanceSetupParams);
256         loadParams.putBinder(ISdkSandboxTestExecutor.TEST_AUTHOR_DEFINED_BINDER, mBinder);
257         final FakeLoadSdkCallback callback = new FakeLoadSdkCallback();
258         mSdkSandboxManager.loadSdk(mSdkName, loadParams, Runnable::run, callback);
259         try {
260             callback.assertLoadSdkIsSuccessful();
261         } catch (IllegalStateException e) {
262             // The underlying {@link WaitableCountDownLatch} used by the
263             // {@link FakeLoadSdkCallback} will throw this exception if we timeout waiting for a
264             // response. In which case, we should _not_ attempt to retrieve the error code because
265             // the getLoadSdkErrorCode call will likely fail as the callback would not have
266             // resolved yet.
267             throw e;
268         } catch (AssertionError | Exception e) {
269             // We cannot use Assume here, since loadTestSdk runs in @BeforeClass.
270             // We allow null and then check for null when test executes.
271             if (callback.getLoadSdkErrorCode() != SdkSandboxManager.LOAD_SDK_SDK_SANDBOX_DISABLED) {
272                 throw e;
273             }
274         }
275         final SandboxedSdk testSdk = callback.getSandboxedSdk();
276         // Allow returned SDK to be null, in the case when SDK sandbox is disabled.
277         if (testSdk == null) {
278             return null;
279         }
280         Assert.assertNotNull(testSdk.getInterface());
281         return testSdk.getInterface();
282     }
283 
284     private void setView(ActivityScenario scenario) throws Exception {
285         AtomicReference<RequestSurfacePackageException> surfacePackageException =
286                 new AtomicReference<>(null);
287         scenario.onActivity(
288                 activity -> {
289                     final SurfaceView renderedView = activity.findViewById(R.id.rendered_view);
290                     final FakeRequestSurfacePackageCallback surfacePackageCallback =
291                             new FakeRequestSurfacePackageCallback();
292 
293                     Bundle params = new Bundle();
294                     params.putInt(EXTRA_WIDTH_IN_PIXELS, renderedView.getWidth());
295                     params.putInt(EXTRA_HEIGHT_IN_PIXELS, renderedView.getHeight());
296                     params.putInt(EXTRA_DISPLAY_ID, activity.getDisplay().getDisplayId());
297                     params.putBinder(EXTRA_HOST_TOKEN, renderedView.getHostToken());
298 
299                     mSdkSandboxManager.requestSurfacePackage(
300                             mSdkName, params, Runnable::run, surfacePackageCallback);
301 
302                     if (!surfacePackageCallback.isRequestSurfacePackageSuccessful()) {
303                         surfacePackageException.set(
304                                 surfacePackageCallback.getSurfacePackageException());
305                     } else {
306                         renderedView.setChildSurfacePackage(
307                                 surfacePackageCallback.getSurfacePackage());
308                         renderedView.setVisibility(View.VISIBLE);
309                         renderedView.setZOrderOnTop(true);
310 
311                         // Keyboard events will only go to the surface view if it has focus.
312                         // This needs to be done with a touch event.
313                         renderedView.setFocusableInTouchMode(true);
314                         renderedView.requestFocusFromTouch();
315                     }
316                 });
317         if (surfacePackageException.get() != null) {
318             throw surfacePackageException.get();
319         }
320     }
321 
322     private boolean isFlagSet(@RuleOptions int flag) {
323         return (mFlags & flag) == flag;
324     }
325 
326     /**
327      * Similar to Callable but uses Throwable instead. Also not generic because we don't need the
328      * return types in this class.
329      */
330     private interface MightThrow {
331         void call() throws Throwable;
332     }
333 }
334