1 /* 2 * Copyright (C) 2017 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.view.inputmethod.cts.util; 18 19 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 20 import static android.view.WindowInsets.Type.displayCutout; 21 import static android.view.WindowInsets.Type.systemBars; 22 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; 23 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 24 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED; 25 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 26 27 import static org.junit.Assert.fail; 28 29 import android.app.Activity; 30 import android.app.ActivityOptions; 31 import android.app.Instrumentation; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.graphics.PixelFormat; 35 import android.os.Bundle; 36 import android.view.View; 37 import android.view.Window; 38 import android.view.WindowManager; 39 import android.widget.TextView; 40 import android.window.OnBackInvokedCallback; 41 import android.window.OnBackInvokedDispatcher; 42 43 import androidx.annotation.AnyThread; 44 import androidx.annotation.NonNull; 45 import androidx.annotation.UiThread; 46 import androidx.test.platform.app.InstrumentationRegistry; 47 48 import com.android.compatibility.common.util.SystemUtil; 49 50 import com.google.common.util.concurrent.SettableFuture; 51 52 import java.util.concurrent.Callable; 53 import java.util.concurrent.TimeUnit; 54 import java.util.concurrent.atomic.AtomicBoolean; 55 import java.util.concurrent.atomic.AtomicReference; 56 import java.util.function.Function; 57 58 public class TestActivity extends Activity { 59 60 public static final String OVERLAY_WINDOW_NAME = "TestActivity.APP_OVERLAY_WINDOW"; 61 private static final AtomicReference<Function<TestActivity, View>> sInitializer = 62 new AtomicReference<>(); 63 64 private Function<TestActivity, View> mInitializer = null; 65 66 private static final AtomicReference<SettableFuture<TestActivity>> sFutureRef = 67 new AtomicReference<>(); 68 private static final long WAIT_TIMEOUT_MS = 5000; 69 70 private AtomicBoolean mIgnoreBackKey = new AtomicBoolean(); 71 72 private long mOnBackPressedCallCount; 73 74 private TextView mOverlayView; 75 private OnBackInvokedCallback mIgnoreBackKeyCallback = () -> { 76 // Ignore back. 77 }; 78 private Boolean mIgnoreBackKeyCallbackRegistered = false; 79 80 private static final Starter DEFAULT_STARTER = new Starter(); 81 82 /** 83 * Controls how {@link #onBackPressed()} behaves. 84 * 85 * <p>TODO: Use {@link android.app.AppComponentFactory} instead to customise the behavior of 86 * {@link TestActivity}.</p> 87 * 88 * @param ignore {@code true} when {@link TestActivity} should do nothing when 89 * {@link #onBackPressed()} is called 90 */ 91 @AnyThread setIgnoreBackKey(boolean ignore)92 public void setIgnoreBackKey(boolean ignore) { 93 mIgnoreBackKey.set(ignore); 94 if (ignore) { 95 if (!mIgnoreBackKeyCallbackRegistered) { 96 getOnBackInvokedDispatcher().registerOnBackInvokedCallback( 97 OnBackInvokedDispatcher.PRIORITY_DEFAULT, mIgnoreBackKeyCallback); 98 mIgnoreBackKeyCallbackRegistered = true; 99 } 100 } else { 101 if (mIgnoreBackKeyCallbackRegistered) { 102 getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback( 103 mIgnoreBackKeyCallback); 104 mIgnoreBackKeyCallbackRegistered = false; 105 } 106 } 107 } 108 109 @UiThread getOnBackPressedCallCount()110 public long getOnBackPressedCallCount() { 111 return mOnBackPressedCallCount; 112 } 113 114 @Override onEnterAnimationComplete()115 public void onEnterAnimationComplete() { 116 super.onEnterAnimationComplete(); 117 118 final SettableFuture<TestActivity> future = sFutureRef.getAndSet(null); 119 if (future != null) { 120 future.set(this); 121 } 122 } 123 124 /** 125 * {@inheritDoc} 126 */ 127 @Override onCreate(Bundle savedInstanceState)128 protected void onCreate(Bundle savedInstanceState) { 129 super.onCreate(savedInstanceState); 130 if (mInitializer == null) { 131 mInitializer = sInitializer.get(); 132 } 133 // Currently SOFT_INPUT_STATE_UNSPECIFIED isn't appropriate for CTS test because there is no 134 // clear spec about how it behaves. In order to make our tests deterministic, currently we 135 // must use SOFT_INPUT_STATE_UNCHANGED. 136 // TODO(Bug 77152727): Remove the following code once we define how 137 // SOFT_INPUT_STATE_UNSPECIFIED actually behaves. 138 setSoftInputState(SOFT_INPUT_STATE_UNCHANGED); 139 setContentView(mInitializer.apply(this)); 140 141 // Add padding for edge-to-edge but return original insets. 142 getWindow().getDecorView().setOnApplyWindowInsetsListener((v, insets) -> { 143 final var i = insets.getInsets(systemBars() | displayCutout()); 144 v.setPadding(i.left, i.top, i.right, i.bottom); 145 return insets; 146 }); 147 } 148 149 @Override onDestroy()150 protected void onDestroy() { 151 super.onDestroy(); 152 if (mOverlayView != null) { 153 mOverlayView.getContext() 154 .getSystemService(WindowManager.class).removeView(mOverlayView); 155 mOverlayView = null; 156 } 157 if (mIgnoreBackKeyCallbackRegistered) { 158 getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mIgnoreBackKeyCallback); 159 mIgnoreBackKeyCallbackRegistered = false; 160 } 161 } 162 163 /** 164 * {@inheritDoc} 165 */ 166 @Override onBackPressed()167 public void onBackPressed() { 168 ++mOnBackPressedCallCount; 169 if (mIgnoreBackKey.get()) { 170 return; 171 } 172 super.onBackPressed(); 173 } 174 showOverlayWindow()175 public void showOverlayWindow() { 176 showOverlayWindow(false /* imeFocusable */); 177 } showOverlayWindow(boolean imeFocusable)178 public void showOverlayWindow(boolean imeFocusable) { 179 if (mOverlayView != null) { 180 throw new IllegalStateException("can only show one overlay at a time."); 181 } 182 SystemUtil.runWithShellPermissionIdentity(() -> { 183 Context overlayContext = getApplicationContext().createWindowContext(getDisplay(), 184 TYPE_APPLICATION_OVERLAY, null); 185 mOverlayView = new TextView(overlayContext); 186 WindowManager.LayoutParams params = new WindowManager.LayoutParams(MATCH_PARENT, 187 MATCH_PARENT, TYPE_APPLICATION_OVERLAY, 188 imeFocusable ? FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM : FLAG_NOT_FOCUSABLE, 189 PixelFormat.TRANSLUCENT); 190 params.setTitle(OVERLAY_WINDOW_NAME); 191 mOverlayView.setLayoutParams(params); 192 mOverlayView.setText("IME CTS TestActivity OverlayView"); 193 mOverlayView.setBackgroundColor(0x77FFFF00); 194 overlayContext.getSystemService(WindowManager.class).addView(mOverlayView, params); 195 }); 196 } 197 198 /** 199 * Launches {@link TestActivity} with the given initialization logic for content view. 200 * 201 * When you need to configure launch options, use {@link Starter} class. 202 * 203 * <p>As long as you are using {@link androidx.test.runner.AndroidJUnitRunner}, the test 204 * runner automatically calls {@link Activity#finish()} for the {@link Activity} launched when 205 * the test finished. You do not need to explicitly call {@link Activity#finish()}.</p> 206 * 207 * @param activityInitializer initializer to supply {@link View} to be passed to 208 * {@link Activity#setContentView(View)} 209 * @return {@link TestActivity} launched 210 */ startSync( @onNull Function<TestActivity, View> activityInitializer)211 public static TestActivity startSync( 212 @NonNull Function<TestActivity, View> activityInitializer) { 213 return DEFAULT_STARTER.startSync(activityInitializer, TestActivity.class); 214 } 215 216 /** 217 * Updates {@link WindowManager.LayoutParams#softInputMode}. 218 * 219 * @param newState One of {@link WindowManager.LayoutParams#SOFT_INPUT_STATE_UNSPECIFIED}, 220 * {@link WindowManager.LayoutParams#SOFT_INPUT_STATE_UNCHANGED}, 221 * {@link WindowManager.LayoutParams#SOFT_INPUT_STATE_HIDDEN}, 222 * {@link WindowManager.LayoutParams#SOFT_INPUT_STATE_ALWAYS_HIDDEN}, 223 * {@link WindowManager.LayoutParams#SOFT_INPUT_STATE_VISIBLE}, 224 * {@link WindowManager.LayoutParams#SOFT_INPUT_STATE_ALWAYS_VISIBLE} 225 */ setSoftInputState(int newState)226 private void setSoftInputState(int newState) { 227 final Window window = getWindow(); 228 final int currentSoftInputMode = window.getAttributes().softInputMode; 229 final int newSoftInputMode = 230 (currentSoftInputMode & ~WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE) 231 | newState; 232 window.setSoftInputMode(newSoftInputMode); 233 } 234 235 /** 236 * Starts TestActivity with given options such as windowing mode, launch target display, etc. 237 * 238 * By default, {@link Intent#FLAG_ACTIVITY_NEW_TASK} and {@link Intent#FLAG_ACTIVITY_CLEAR_TASK} 239 * are given to {@link Intent#setFlags(int)}. This can be changed by using some methods. 240 */ 241 public static class Starter { 242 private static final int DEFAULT_FLAGS = 243 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK; 244 245 private int mFlags = 0; 246 private int mAdditionalFlags = 0; 247 private ActivityOptions mOptions = null; 248 private boolean mRequireShellPermission = false; 249 Starter()250 public Starter() { 251 } 252 253 /** 254 * Specifies an additional flags to be given to {@link Intent#setFlags(int)}. 255 */ withAdditionalFlags(int additionalFlags)256 public Starter withAdditionalFlags(int additionalFlags) { 257 mAdditionalFlags |= additionalFlags; 258 return this; 259 } 260 261 /** 262 * Specifies {@link android.app.WindowConfiguration.WindowingMode a windowing mode} that the 263 * activity is launched in. 264 */ withWindowingMode(int windowingMode)265 public Starter withWindowingMode(int windowingMode) { 266 if (mOptions == null) { 267 mOptions = ActivityOptions.makeBasic(); 268 } 269 mOptions.setLaunchWindowingMode(windowingMode); 270 return this; 271 } 272 273 /** 274 * Specifies a target display ID that the activity is launched in. 275 */ withDisplayId(int displayId)276 public Starter withDisplayId(int displayId) { 277 if (mOptions == null) { 278 mOptions = ActivityOptions.makeBasic(); 279 } 280 mOptions.setLaunchDisplayId(displayId); 281 mRequireShellPermission = true; 282 return this; 283 } 284 285 /** 286 * Uses {@link Intent#FLAG_ACTIVITY_NEW_TASK} and {@link Intent#FLAG_ACTIVITY_NEW_DOCUMENT} 287 * for {@link Intent#setFlags(int)}. 288 */ asNewTask()289 public Starter asNewTask() { 290 if (mFlags != 0) { 291 throw new IllegalStateException("Conflicting flags are specified."); 292 } 293 mFlags = Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT; 294 return this; 295 } 296 297 /** 298 * Uses {@link Intent#FLAG_ACTIVITY_NEW_TASK} and {@link Intent#FLAG_ACTIVITY_MULTIPLE_TASK} 299 * for {@link Intent#setFlags(int)}. 300 */ asMultipleTask()301 public Starter asMultipleTask() { 302 if (mFlags != 0) { 303 throw new IllegalStateException("Conflicting flags are specified."); 304 } 305 mFlags = Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK; 306 return this; 307 } 308 309 /** 310 * Uses {@link Intent#FLAG_ACTIVITY_NEW_TASK} and {@link Intent#FLAG_ACTIVITY_CLEAR_TOP} 311 * for {@link Intent#setFlags(int)}. 312 */ asSameTaskAndClearTop()313 public Starter asSameTaskAndClearTop() { 314 if (mFlags != 0) { 315 throw new IllegalStateException("Conflicting flags are specified."); 316 } 317 mFlags = Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP; 318 return this; 319 } 320 321 /** 322 * Launches {@link TestActivity} with the given initialization logic for content view 323 * with already specified parameters. 324 * 325 * <p>As long as you are using {@link androidx.test.runner.AndroidJUnitRunner}, the test 326 * runner automatically calls {@link Activity#finish()} for the {@link Activity} launched 327 * when the test finished. You do not need to explicitly call {@link Activity#finish()}.</p> 328 * 329 * @param activityInitializer initializer to supply {@link View} to be passed to 330 * {@link Activity#setContentView(View)} 331 * @param activityClass the target class to start, which extends {@link TestActivity} 332 * @return {@link TestActivity} launched 333 */ startSync(@onNull Function<TestActivity, View> activityInitializer, Class<? extends TestActivity> activityClass)334 public TestActivity startSync(@NonNull Function<TestActivity, View> activityInitializer, 335 Class<? extends TestActivity> activityClass) { 336 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 337 sInitializer.set(activityInitializer); 338 339 if (mFlags == 0) { 340 mFlags = DEFAULT_FLAGS; 341 } 342 final Intent intent = new Intent() 343 .setAction(Intent.ACTION_MAIN) 344 .setClass(instrumentation.getContext(), activityClass) 345 .addFlags(mFlags | mAdditionalFlags); 346 final Callable<TestActivity> launcher = 347 () -> (TestActivity) instrumentation.startActivitySync( 348 intent, mOptions == null ? null : mOptions.toBundle()); 349 350 try { 351 if (mRequireShellPermission) { 352 return SystemUtil.callWithShellPermissionIdentity(launcher); 353 } else { 354 return launcher.call(); 355 } 356 } catch (Exception e) { 357 fail("Failed to start TestActivity: " + e); 358 return null; 359 } 360 } 361 362 /** 363 * Launches {@link TestActivity} from the given source activity with the given 364 * initialization logic for content view with already specified parameters. 365 * 366 * <p>As long as you are using {@link androidx.test.runner.AndroidJUnitRunner}, the test 367 * runner automatically calls {@link Activity#finish()} for the {@link Activity} launched 368 * when the test finished. You do not need to explicitly call {@link Activity#finish()}.</p> 369 * 370 * @param fromActivity the source activity requests launching the target 371 * @param activityInitializer initializer to supply {@link View} to be passed to 372 * {@link Activity#setContentView(View)} 373 * @param activityClass the target class to start, which extends {@link TestActivity} 374 * @return {@link TestActivity} launched 375 */ startSync(@onNull Activity fromActivity, @NonNull Function<TestActivity, View> activityInitializer, Class<? extends TestActivity> activityClass)376 public TestActivity startSync(@NonNull Activity fromActivity, 377 @NonNull Function<TestActivity, View> activityInitializer, 378 Class<? extends TestActivity> activityClass) { 379 sInitializer.set(activityInitializer); 380 381 if (mFlags == 0) { 382 mFlags = DEFAULT_FLAGS; 383 } 384 final Intent intent = new Intent() 385 .setAction(Intent.ACTION_MAIN) 386 .setClass(fromActivity, activityClass) 387 .addFlags(mFlags | mAdditionalFlags); 388 final Callable<TestActivity> launcher = () -> { 389 fromActivity.startActivity(intent, mOptions == null ? null : mOptions.toBundle()); 390 final SettableFuture<TestActivity> future = SettableFuture.create(); 391 sFutureRef.set(future); 392 return future.get(WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 393 }; 394 try { 395 if (mRequireShellPermission) { 396 return SystemUtil.callWithShellPermissionIdentity(launcher); 397 } else { 398 return launcher.call(); 399 } 400 } catch (Exception e) { 401 fail("Failed to start TestActivity: " + e); 402 return null; 403 } 404 } 405 } 406 } 407