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