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