• 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.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