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 final int testUserId = UserHandle.myUserId(); 152 if (instant) { 153 // Override app-links domain verification. 154 runShellCommandOrThrow( 155 String.format("pm set-app-links-user-selection --user %d --package %s true %s", 156 testUserId, TEST_ACTIVITY.getPackageName(), 157 TEST_ACTIVITY_URI.getHost())); 158 final Uri uri = formatStringIntentParam(TEST_ACTIVITY_URI, extras); 159 commandBuilder.append(String.format("am start -a %s -c %s --activity-clear-task %s", 160 Intent.ACTION_VIEW, Intent.CATEGORY_BROWSABLE, uri.toString())); 161 } else { 162 commandBuilder.append( 163 String.format("am start -a %s -n %s --user %d --activity-clear-task", 164 Intent.ACTION_MAIN, TEST_ACTIVITY.flattenToShortString(), testUserId)); 165 if (extras != null) { 166 extras.forEach((key, value) -> commandBuilder.append(" --es ") 167 .append(key).append(" ").append(value)); 168 } 169 } 170 171 runWithShellPermissionIdentity(() -> { 172 runShellCommandOrThrow(commandBuilder.toString()); 173 }); 174 UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 175 BySelector activitySelector = By.pkg(TEST_ACTIVITY.getPackageName()).depth(0); 176 uiDevice.wait(Until.hasObject(activitySelector), timeout); 177 178 // Make sure to stop package after test finished for resource reclaim. 179 return () -> TestUtils.forceStopPackage(TEST_ACTIVITY.getPackageName()); 180 } 181 182 /** 183 * Launches {@link android.view.inputmethod.ctstestapp.MainActivity}. 184 * 185 * @param userId the user id for which the Activity should be started 186 * @param instant {@code true} when the Activity is installed as an instant app. 187 * @param extras extra parameters to be passed to the Activity. 188 * @return {@link AutoCloseable} object to automatically stop the test Activity package. 189 */ launchAsUser(int userId, boolean instant, @Nullable Map<String, String> extras)190 public static AutoCloseable launchAsUser(int userId, boolean instant, 191 @Nullable Map<String, String> extras) { 192 final StringBuilder commandBuilder = new StringBuilder(); 193 if (instant) { 194 // Override app-links domain verification. 195 runShellCommandOrThrow( 196 String.format("pm set-app-links-user-selection --user %d --package %s true %s", 197 userId, TEST_ACTIVITY.getPackageName(), TEST_ACTIVITY_URI.getHost())); 198 final Uri uri = formatStringIntentParam(TEST_ACTIVITY_URI, extras); 199 commandBuilder.append( 200 String.format("am start -a %s -c %s --user %d --activity-clear-task %s", 201 Intent.ACTION_VIEW, Intent.CATEGORY_BROWSABLE, userId, uri.toString())); 202 } else { 203 commandBuilder.append( 204 String.format("am start -a %s -n %s --user %d --activity-clear-task", 205 Intent.ACTION_MAIN, TEST_ACTIVITY.flattenToShortString(), userId)); 206 if (extras != null) { 207 extras.forEach((key, value) -> commandBuilder.append(" --es ") 208 .append(key).append(" ").append(value)); 209 } 210 } 211 212 runWithShellPermissionIdentity(() -> { 213 runShellCommandOrThrow(commandBuilder.toString()); 214 }); 215 // Make sure to stop package after test finished for resource reclaim. 216 return () -> TestUtils.forceStopPackage(TEST_ACTIVITY.getPackageName(), userId); 217 } 218 219 /** 220 * Launches {@link android.view.inputmethod.ctstestapp.MainActivity}. 221 * 222 * @param userId the user id for which the Activity should be started 223 * @param instant {@code true} when the Activity is installed as an instant app. 224 * @param extras extra parameters to be passed to the Activity. 225 * @param onCreateInputConnectionCallback the callback that will be invoked, once the input 226 * connection was created 227 * @return {@link AutoCloseable} object to automatically stop the test Activity package. 228 */ launchSyncAsUser(int userId, boolean instant, @Nullable Map<String, String> extras, RemoteCallback onCreateInputConnectionCallback)229 public static AutoCloseable launchSyncAsUser(int userId, boolean instant, 230 @Nullable Map<String, String> extras, RemoteCallback onCreateInputConnectionCallback) { 231 Context context = InstrumentationRegistry.getInstrumentation().getContext(); 232 final Intent intent = new Intent().setClassName(TEST_ACTIVITY.getPackageName(), 233 TEST_ACTIVITY.getClassName()).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); 234 if (extras != null) { 235 extras.forEach(intent::putExtra); 236 } 237 if (onCreateInputConnectionCallback != null) { 238 intent.putExtra(EXTRA_ON_CREATE_INPUT_CONNECTION_CALLBACK, 239 onCreateInputConnectionCallback); 240 intent.putExtra(EXTRA_ON_CREATE_USER_HANDLE_SESSION_ID, 241 Long.toString(SystemClock.elapsedRealtimeNanos())); 242 } 243 244 if (instant) { 245 // Override app-links domain verification. 246 runShellCommand( 247 String.format("pm set-app-links-user-selection --user %s --package %s true %s", 248 userId, TEST_ACTIVITY.getPackageName(), TEST_ACTIVITY_URI.getHost())); 249 intent.setAction(Intent.ACTION_VIEW).addCategory(Intent.CATEGORY_BROWSABLE); 250 intent.setData(TEST_ACTIVITY_URI); 251 } else { 252 intent.setAction(Intent.ACTION_MAIN); 253 } 254 runWithShellPermissionIdentity(() -> { 255 context.startActivityAsUser(intent, UserHandle.of(userId)); 256 }, Manifest.permission.INTERACT_ACROSS_USERS_FULL); 257 258 // Make sure to stop package after test finished for resource reclaim. 259 return () -> TestUtils.forceStopPackage(TEST_ACTIVITY.getPackageName(), userId); 260 } 261 262 /** 263 * Sends a broadcast to {@link "android.view.inputmethod.ctstestapp.MainActivity"}. 264 * 265 * @param extra {@link #EXTRA_DISMISS_DIALOG} or {@link #EXTRA_SHOW_SOFT_INPUT}. 266 */ sendBroadcastAction(String extra)267 public static void sendBroadcastAction(String extra) { 268 final StringBuilder commandBuilder = new StringBuilder(); 269 commandBuilder.append("am broadcast -a ").append(ACTION_TRIGGER).append(" -p ").append( 270 TEST_ACTIVITY.getPackageName()); 271 commandBuilder.append(" -f 0x").append( 272 Integer.toHexString(FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS)); 273 commandBuilder.append(" --receiver-registered-only"); 274 commandBuilder.append(" --ez " + extra + " true"); 275 runWithShellPermissionIdentity(() -> { 276 runShellCommand(commandBuilder.toString()); 277 }); 278 } 279 280 /** 281 * Sends a broadcast to {@link android.view.inputmethod.ctstestapp.MainActivity}. 282 * 283 * @param extra {@link #EXTRA_DISMISS_DIALOG} or {@link #EXTRA_SHOW_SOFT_INPUT}. 284 * @param userId The target user ID. 285 */ sendBroadcastAction(String extra, int userId)286 public static void sendBroadcastAction(String extra, int userId) { 287 final StringBuilder commandBuilder = new StringBuilder(); 288 commandBuilder.append("am broadcast -a ").append(ACTION_TRIGGER).append(" -p ").append( 289 TEST_ACTIVITY.getPackageName()); 290 commandBuilder.append(" -f 0x").append( 291 Integer.toHexString(FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS)); 292 commandBuilder.append(" --receiver-registered-only"); 293 commandBuilder.append(" --user " + userId); 294 commandBuilder.append(" --ez " + extra + " true"); 295 runWithShellPermissionIdentity(() -> { 296 runShellCommand(commandBuilder.toString()); 297 }); 298 } 299 300 /** 301 * Force-stops {@link "android.view.inputmethod.ctstestapp"} package. 302 */ forceStopPackage()303 public static void forceStopPackage() { 304 TestUtils.forceStopPackage(TEST_ACTIVITY.getPackageName()); 305 } 306 307 /** 308 * @return {@code "android.view.inputmethod.ctstestapp"}. 309 */ getPackageName()310 public static String getPackageName() { 311 return TEST_ACTIVITY.getPackageName(); 312 } 313 } 314