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