1 /*
2  * Copyright (C) 2021 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.content.Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS;
20 
21 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
22 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
23 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
24 
25 import android.Manifest;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.net.Uri;
30 import android.os.RemoteCallback;
31 import android.os.SystemClock;
32 import android.os.UserHandle;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.Nullable;
36 import androidx.test.platform.app.InstrumentationRegistry;
37 import androidx.test.uiautomator.By;
38 import androidx.test.uiautomator.BySelector;
39 import androidx.test.uiautomator.UiDevice;
40 import androidx.test.uiautomator.Until;
41 
42 import java.util.Map;
43 
44 /**
45  * Provides constants and utility methods to interact with
46  * {@link android.view.inputmethod.ctstestapp.MainActivity}.
47  */
48 public final class MockTestActivityUtil {
49     public static final ComponentName TEST_ACTIVITY = new ComponentName(
50             "android.view.inputmethod.ctstestapp",
51             "android.view.inputmethod.ctstestapp.MainActivity");
52     private static final Uri TEST_ACTIVITY_URI =
53             Uri.parse("https://example.com/android/view/inputmethod/ctstestapp");
54 
55     public static final String ACTION_TRIGGER = "broadcast_action_trigger";
56 
57     /**
58      * A key to be used as the {@code key} of {@link Map} passed as {@code extras} parameter of
59      * {@link #launchSync(boolean, long, Map)}.
60      *
61      * <p>A valid {@code value} is the string representation of an integer.
62      */
63     public static final String EXTRA_SOFT_INPUT_MODE =
64             "android.view.inputmethod.ctstestapp.EXTRA_SOFT_INPUT_MODE";
65 
66     /**
67      * A key to be used as the {@code key} of {@link Map} passed as {@code extras} parameter of
68      * {@link #launchSync(boolean, long, Map)}.
69      *
70      * <p>A valid {@code value} is either {@code "true"} or {@code "false"}.</p>
71      */
72     public static final String EXTRA_KEY_SHOW_DIALOG =
73             "android.view.inputmethod.ctstestapp.EXTRA_KEY_SHOW_DIALOG";
74 
75     /**
76      * A key to be used as the {@code key} of {@link Map} passed as {@code extras} parameter of
77      * {@link #launchSync(boolean, long, Map)}.
78      *
79      * <p>The specified {@code value} will be set to
80      * {@link android.view.inputmethod.EditorInfo#privateImeOptions}.</p>
81      */
82     public static final String EXTRA_KEY_PRIVATE_IME_OPTIONS =
83             "android.view.inputmethod.ctstestapp.EXTRA_KEY_PRIVATE_IME_OPTIONS";
84 
85     /**
86      * Can be passed to {@link #sendBroadcastAction(String)} to dismiss the dialog box if exists.
87      */
88     public static final String EXTRA_DISMISS_DIALOG = "extra_dismiss_dialog";
89 
90     /**
91      * Can be passed to {@link #sendBroadcastAction(String)} call
92      * {@link android.view.inputmethod.InputMethodManager#showSoftInput(android.view.View, int)}.
93      */
94     public static final String EXTRA_SHOW_SOFT_INPUT = "extra_show_soft_input";
95 
96     /**
97      * Can be passed to {@link #sendBroadcastAction(String)} to declare editor as a
98      * {@link android.view.View#setIsHandwritingDelegate(boolean) handwriting delegate}.
99      */
100     public static final String EXTRA_HANDWRITING_DELEGATE = "extra_handwriting_delegate";
101 
102     /**
103      * Can be passed to {@link #sendBroadcastAction(String)} to declare editor as {@link
104      * android.view.View#setHomeScreenHandwritingDelegatorAllowed(boolean)}.
105      */
106     public static final String EXTRA_HOME_HANDWRITING_DELEGATOR_ALLOWED =
107             "extra_home_handwriting_delegator_allowed";
108 
109     /**
110      * Is used by the {@link RemoteCallback} in launchSyncAsUser()
111      */
112     public static final String ACTION_KEY_REPLY_USER_HANDLE =
113             "android.inputmethodservice.cts.ime.ReplyUserHandle";
114     public static final String EXTRA_ON_CREATE_INPUT_CONNECTION_CALLBACK =
115             "extra_on_create_input_connection_callback";
116     public static final String EXTRA_ON_CREATE_USER_HANDLE_SESSION_ID =
117             "extra_on_create_user_handle_session_id";
118 
119     @NonNull
formatStringIntentParam(@onNull Uri uri, Map<String, String> extras)120     private static Uri formatStringIntentParam(@NonNull Uri uri, Map<String, String> extras) {
121         if (extras == null) {
122             return uri;
123         }
124         final Uri.Builder builder = uri.buildUpon();
125         extras.forEach(builder::appendQueryParameter);
126         return builder.build();
127     }
128 
129     /**
130      * Launches {@link "android.view.inputmethod.ctstestapp.MainActivity"}.
131      *
132      * @param instant {@code true} when the Activity is installed as an instant app.
133      * @param timeout the timeout to wait until the Activity becomes ready.
134      * @return {@link AutoCloseable} object to automatically stop the test Activity package.
135      */
launchSync(boolean instant, long timeout)136     public static AutoCloseable launchSync(boolean instant, long timeout) {
137         return launchSync(instant, timeout, null);
138     }
139 
140     /**
141      * Launches {@link "android.view.inputmethod.ctstestapp.MainActivity"}.
142      *
143      * @param instant {@code true} when the Activity is installed as an instant app.
144      * @param timeout the timeout to wait until the Activity becomes ready.
145      * @param extras extra parameters to be passed to the Activity.
146      * @return {@link AutoCloseable} object to automatically stop the test Activity package.
147      */
launchSync(boolean instant, long timeout, @Nullable Map<String, String> extras)148     public static AutoCloseable launchSync(boolean instant, long timeout,
149             @Nullable Map<String, String> extras) {
150         final StringBuilder commandBuilder = new StringBuilder();
151         if (instant) {
152             // Override app-links domain verification.
153             runShellCommandOrThrow(
154                     String.format("pm set-app-links-user-selection --user cur --package %s true %s",
155                             TEST_ACTIVITY.getPackageName(), TEST_ACTIVITY_URI.getHost()));
156             final Uri uri = formatStringIntentParam(TEST_ACTIVITY_URI, extras);
157             commandBuilder.append(String.format("am start -a %s -c %s --activity-clear-task %s",
158                     Intent.ACTION_VIEW, Intent.CATEGORY_BROWSABLE, uri.toString()));
159         } else {
160             commandBuilder.append(String.format("am start -a %s -n %s --activity-clear-task",
161                     Intent.ACTION_MAIN, TEST_ACTIVITY.flattenToShortString()));
162             if (extras != null) {
163                 extras.forEach((key, value) -> commandBuilder.append(" --es ")
164                         .append(key).append(" ").append(value));
165             }
166         }
167 
168         runWithShellPermissionIdentity(() -> {
169             runShellCommandOrThrow(commandBuilder.toString());
170         });
171         UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
172         BySelector activitySelector = By.pkg(TEST_ACTIVITY.getPackageName()).depth(0);
173         uiDevice.wait(Until.hasObject(activitySelector), timeout);
174 
175         // Make sure to stop package after test finished for resource reclaim.
176         return () -> TestUtils.forceStopPackage(TEST_ACTIVITY.getPackageName());
177     }
178 
179     /**
180      * Launches {@link android.view.inputmethod.ctstestapp.MainActivity}.
181      *
182      * @param userId the user id for which the Activity should be started
183      * @param instant {@code true} when the Activity is installed as an instant app.
184      * @param extras extra parameters to be passed to the Activity.
185      * @return {@link AutoCloseable} object to automatically stop the test Activity package.
186      */
launchAsUser(int userId, boolean instant, @Nullable Map<String, String> extras)187     public static AutoCloseable launchAsUser(int userId, boolean instant,
188             @Nullable Map<String, String> extras) {
189         final StringBuilder commandBuilder = new StringBuilder();
190         if (instant) {
191             // Override app-links domain verification.
192             runShellCommandOrThrow(
193                     String.format("pm set-app-links-user-selection --user %d --package %s true %s",
194                             userId, TEST_ACTIVITY.getPackageName(), TEST_ACTIVITY_URI.getHost()));
195             final Uri uri = formatStringIntentParam(TEST_ACTIVITY_URI, extras);
196             commandBuilder.append(
197                     String.format("am start -a %s -c %s --user %d --activity-clear-task %s",
198                             Intent.ACTION_VIEW, Intent.CATEGORY_BROWSABLE, userId, uri.toString()));
199         } else {
200             commandBuilder.append(
201                     String.format("am start -a %s -n %s --user %d --activity-clear-task",
202                             Intent.ACTION_MAIN, TEST_ACTIVITY.flattenToShortString(), userId));
203             if (extras != null) {
204                 extras.forEach((key, value) -> commandBuilder.append(" --es ")
205                         .append(key).append(" ").append(value));
206             }
207         }
208 
209         runWithShellPermissionIdentity(() -> {
210             runShellCommandOrThrow(commandBuilder.toString());
211         });
212         // Make sure to stop package after test finished for resource reclaim.
213         return () -> TestUtils.forceStopPackage(TEST_ACTIVITY.getPackageName(), userId);
214     }
215 
216     /**
217      * Launches {@link android.view.inputmethod.ctstestapp.MainActivity}.
218      *
219      * @param userId the user id for which the Activity should be started
220      * @param instant {@code true} when the Activity is installed as an instant app.
221      * @param extras extra parameters to be passed to the Activity.
222      * @param onCreateInputConnectionCallback the callback that will be invoked, once the input
223      *                                        connection was created
224      * @return {@link AutoCloseable} object to automatically stop the test Activity package.
225      */
launchSyncAsUser(int userId, boolean instant, @Nullable Map<String, String> extras, RemoteCallback onCreateInputConnectionCallback)226     public static AutoCloseable launchSyncAsUser(int userId, boolean instant,
227             @Nullable Map<String, String> extras, RemoteCallback onCreateInputConnectionCallback) {
228         Context context = InstrumentationRegistry.getInstrumentation().getContext();
229         final Intent intent = new Intent().setClassName(TEST_ACTIVITY.getPackageName(),
230                 TEST_ACTIVITY.getClassName()).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
231         if (extras != null) {
232             extras.forEach(intent::putExtra);
233         }
234         if (onCreateInputConnectionCallback != null) {
235             intent.putExtra(EXTRA_ON_CREATE_INPUT_CONNECTION_CALLBACK,
236                     onCreateInputConnectionCallback);
237             intent.putExtra(EXTRA_ON_CREATE_USER_HANDLE_SESSION_ID,
238                     Long.toString(SystemClock.elapsedRealtimeNanos()));
239         }
240 
241         if (instant) {
242             // Override app-links domain verification.
243             runShellCommand(
244                     String.format("pm set-app-links-user-selection --user %s --package %s true %s",
245                             userId, TEST_ACTIVITY.getPackageName(), TEST_ACTIVITY_URI.getHost()));
246             intent.setAction(Intent.ACTION_VIEW).addCategory(Intent.CATEGORY_BROWSABLE);
247             intent.setData(TEST_ACTIVITY_URI);
248         } else {
249             intent.setAction(Intent.ACTION_MAIN);
250         }
251         runWithShellPermissionIdentity(() -> {
252             context.startActivityAsUser(intent, UserHandle.of(userId));
253         }, Manifest.permission.INTERACT_ACROSS_USERS_FULL);
254 
255         // Make sure to stop package after test finished for resource reclaim.
256         return () -> TestUtils.forceStopPackage(TEST_ACTIVITY.getPackageName(), userId);
257     }
258 
259     /**
260      * Sends a broadcast to {@link "android.view.inputmethod.ctstestapp.MainActivity"}.
261      *
262      * @param extra {@link #EXTRA_DISMISS_DIALOG} or {@link #EXTRA_SHOW_SOFT_INPUT}.
263      */
sendBroadcastAction(String extra)264     public static void sendBroadcastAction(String extra) {
265         final StringBuilder commandBuilder = new StringBuilder();
266         commandBuilder.append("am broadcast -a ").append(ACTION_TRIGGER).append(" -p ").append(
267                 TEST_ACTIVITY.getPackageName());
268         commandBuilder.append(" -f 0x").append(
269                 Integer.toHexString(FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS));
270         commandBuilder.append(" --receiver-registered-only");
271         commandBuilder.append(" --ez " + extra + " true");
272         runWithShellPermissionIdentity(() -> {
273             runShellCommand(commandBuilder.toString());
274         });
275     }
276 
277     /**
278      * Sends a broadcast to {@link android.view.inputmethod.ctstestapp.MainActivity}.
279      *
280      * @param extra {@link #EXTRA_DISMISS_DIALOG} or {@link #EXTRA_SHOW_SOFT_INPUT}.
281      * @param userId The target user ID.
282      */
sendBroadcastAction(String extra, int userId)283     public static void sendBroadcastAction(String extra, int userId) {
284         final StringBuilder commandBuilder = new StringBuilder();
285         commandBuilder.append("am broadcast -a ").append(ACTION_TRIGGER).append(" -p ").append(
286                 TEST_ACTIVITY.getPackageName());
287         commandBuilder.append(" -f 0x").append(
288                 Integer.toHexString(FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS));
289         commandBuilder.append(" --receiver-registered-only");
290         commandBuilder.append(" --user " + userId);
291         commandBuilder.append(" --ez " + extra + " true");
292         runWithShellPermissionIdentity(() -> {
293             runShellCommand(commandBuilder.toString());
294         });
295     }
296 
297     /**
298      * Force-stops {@link "android.view.inputmethod.ctstestapp"} package.
299      */
forceStopPackage()300     public static void forceStopPackage() {
301         TestUtils.forceStopPackage(TEST_ACTIVITY.getPackageName());
302     }
303 
304     /**
305      * @return {@code "android.view.inputmethod.ctstestapp"}.
306      */
getPackageName()307     public static String getPackageName() {
308         return TEST_ACTIVITY.getPackageName();
309     }
310 }
311