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