• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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