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