1 /* 2 * Copyright (C) 2013 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 com.android.server.inputmethod; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.UserHandleAware; 22 import android.annotation.UserIdInt; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.pm.ApplicationInfo; 26 import android.content.pm.PackageManager; 27 import android.content.pm.PackageManagerInternal; 28 import android.content.pm.ResolveInfo; 29 import android.content.res.Resources; 30 import android.os.Build; 31 import android.os.UserHandle; 32 import android.provider.Settings; 33 import android.text.TextUtils; 34 import android.util.ArraySet; 35 import android.util.Slog; 36 import android.view.inputmethod.InputMethodInfo; 37 import android.view.inputmethod.InputMethodSubtype; 38 import android.view.textservice.SpellCheckerInfo; 39 40 import com.android.internal.annotations.VisibleForTesting; 41 import com.android.internal.inputmethod.StartInputFlags; 42 import com.android.server.LocalServices; 43 import com.android.server.pm.UserManagerInternal; 44 import com.android.server.textservices.TextServicesManagerInternal; 45 46 import java.io.PrintWriter; 47 import java.util.ArrayList; 48 import java.util.List; 49 import java.util.StringJoiner; 50 import java.util.function.Consumer; 51 52 /** 53 * This class provides random static utility methods for {@link InputMethodManagerService} and its 54 * utility classes. 55 * 56 * <p>This class is intentionally package-private. Utility methods here are tightly coupled with 57 * implementation details in {@link InputMethodManagerService}. Hence this class is not suitable 58 * for other components to directly use.</p> 59 */ 60 final class InputMethodUtils { 61 public static final boolean DEBUG = false; 62 static final int NOT_A_SUBTYPE_ID = -1; 63 private static final String TAG = "InputMethodUtils"; 64 65 // The string for enabled input method is saved as follows: 66 // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0") 67 static final char INPUT_METHOD_SEPARATOR = ':'; 68 static final char INPUT_METHOD_SUBTYPE_SEPARATOR = ';'; 69 InputMethodUtils()70 private InputMethodUtils() { 71 // This utility class is not publicly instantiable. 72 } 73 74 // ---------------------------------------------------------------------- 75 canAddToLastInputMethod(InputMethodSubtype subtype)76 static boolean canAddToLastInputMethod(InputMethodSubtype subtype) { 77 if (subtype == null) return true; 78 return !subtype.isAuxiliary(); 79 } 80 81 @UserHandleAware setNonSelectedSystemImesDisabledUntilUsed(PackageManager packageManagerForUser, List<InputMethodInfo> enabledImis)82 static void setNonSelectedSystemImesDisabledUntilUsed(PackageManager packageManagerForUser, 83 List<InputMethodInfo> enabledImis) { 84 if (DEBUG) { 85 Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed"); 86 } 87 final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray( 88 com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes); 89 if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) { 90 return; 91 } 92 // Only the current spell checker should be treated as an enabled one. 93 final SpellCheckerInfo currentSpellChecker = 94 TextServicesManagerInternal.get().getCurrentSpellCheckerForUser( 95 packageManagerForUser.getUserId()); 96 for (final String packageName : systemImesDisabledUntilUsed) { 97 if (DEBUG) { 98 Slog.d(TAG, "check " + packageName); 99 } 100 boolean enabledIme = false; 101 for (int j = 0; j < enabledImis.size(); ++j) { 102 final InputMethodInfo imi = enabledImis.get(j); 103 if (packageName.equals(imi.getPackageName())) { 104 enabledIme = true; 105 break; 106 } 107 } 108 if (enabledIme) { 109 // enabled ime. skip 110 continue; 111 } 112 if (currentSpellChecker != null 113 && packageName.equals(currentSpellChecker.getPackageName())) { 114 // enabled spell checker. skip 115 if (DEBUG) { 116 Slog.d(TAG, packageName + " is the current spell checker. skip"); 117 } 118 continue; 119 } 120 ApplicationInfo ai; 121 try { 122 ai = packageManagerForUser.getApplicationInfo(packageName, 123 PackageManager.ApplicationInfoFlags.of( 124 PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS)); 125 } catch (PackageManager.NameNotFoundException e) { 126 // This is not an error. No need to show scary error messages. 127 if (DEBUG) { 128 Slog.d(TAG, packageName 129 + " does not exist for userId=" + packageManagerForUser.getUserId()); 130 } 131 continue; 132 } 133 if (ai == null) { 134 // No app found for packageName 135 continue; 136 } 137 final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0; 138 if (!isSystemPackage) { 139 continue; 140 } 141 setDisabledUntilUsed(packageManagerForUser, packageName); 142 } 143 } 144 setDisabledUntilUsed(PackageManager packageManagerForUser, String packageName)145 private static void setDisabledUntilUsed(PackageManager packageManagerForUser, 146 String packageName) { 147 final int state; 148 try { 149 state = packageManagerForUser.getApplicationEnabledSetting(packageName); 150 } catch (IllegalArgumentException e) { 151 Slog.w(TAG, "getApplicationEnabledSetting failed. packageName=" + packageName 152 + " userId=" + packageManagerForUser.getUserId(), e); 153 return; 154 } 155 if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT 156 || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { 157 if (DEBUG) { 158 Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED"); 159 } 160 try { 161 packageManagerForUser.setApplicationEnabledSetting(packageName, 162 PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED, 163 0 /* newState */); 164 } catch (IllegalArgumentException e) { 165 Slog.w(TAG, "setApplicationEnabledSetting failed. packageName=" + packageName 166 + " userId=" + packageManagerForUser.getUserId(), e); 167 return; 168 } 169 } else { 170 if (DEBUG) { 171 Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED"); 172 } 173 } 174 } 175 176 /** 177 * Returns true if a package name belongs to a UID. 178 * 179 * <p>This is a simple wrapper of 180 * {@link PackageManagerInternal#getPackageUid(String, long, int)}.</p> 181 * @param packageManagerInternal the {@link PackageManagerInternal} object to be used for the 182 * validation. 183 * @param uid the UID to be validated. 184 * @param packageName the package name. 185 * @return {@code true} if the package name belongs to the UID. 186 */ checkIfPackageBelongsToUid(PackageManagerInternal packageManagerInternal, int uid, String packageName)187 static boolean checkIfPackageBelongsToUid(PackageManagerInternal packageManagerInternal, 188 int uid, String packageName) { 189 // PackageManagerInternal#getPackageUid() doesn't check MATCH_INSTANT/MATCH_APEX as of 190 // writing. So setting 0 should be fine. 191 return packageManagerInternal.isSameApp(packageName, /* flags= */ 0, uid, 192 UserHandle.getUserId(uid)); 193 } 194 isSoftInputModeStateVisibleAllowed(int targetSdkVersion, @StartInputFlags int startInputFlags)195 static boolean isSoftInputModeStateVisibleAllowed(int targetSdkVersion, 196 @StartInputFlags int startInputFlags) { 197 if (targetSdkVersion < Build.VERSION_CODES.P) { 198 // for compatibility. 199 return true; 200 } 201 if ((startInputFlags & StartInputFlags.VIEW_HAS_FOCUS) == 0) { 202 return false; 203 } 204 if ((startInputFlags & StartInputFlags.IS_TEXT_EDITOR) == 0) { 205 return false; 206 } 207 return true; 208 } 209 210 /** 211 * Converts a user ID, which can be a pseudo user ID such as {@link UserHandle#USER_ALL} to a 212 * list of real user IDs. 213 * 214 * @param userIdToBeResolved A user ID. Two pseudo user ID {@link UserHandle#USER_CURRENT} and 215 * {@link UserHandle#USER_ALL} are also supported 216 * @param currentUserId A real user ID, which will be used when {@link UserHandle#USER_CURRENT} 217 * is specified in {@code userIdToBeResolved}. 218 * @param warningWriter A {@link PrintWriter} to output some debug messages. {@code null} if 219 * no debug message is required. 220 * @return An integer array that contain user IDs. 221 */ resolveUserId(@serIdInt int userIdToBeResolved, @UserIdInt int currentUserId, @Nullable PrintWriter warningWriter)222 static int[] resolveUserId(@UserIdInt int userIdToBeResolved, 223 @UserIdInt int currentUserId, @Nullable PrintWriter warningWriter) { 224 final UserManagerInternal userManagerInternal = 225 LocalServices.getService(UserManagerInternal.class); 226 227 if (userIdToBeResolved == UserHandle.USER_ALL) { 228 return userManagerInternal.getUserIds(); 229 } 230 231 final int sourceUserId; 232 if (userIdToBeResolved == UserHandle.USER_CURRENT) { 233 sourceUserId = currentUserId; 234 } else if (userIdToBeResolved < 0) { 235 if (warningWriter != null) { 236 warningWriter.print("Pseudo user ID "); 237 warningWriter.print(userIdToBeResolved); 238 warningWriter.println(" is not supported."); 239 } 240 return new int[]{}; 241 } else if (userManagerInternal.exists(userIdToBeResolved)) { 242 sourceUserId = userIdToBeResolved; 243 } else { 244 if (warningWriter != null) { 245 warningWriter.print("User #"); 246 warningWriter.print(userIdToBeResolved); 247 warningWriter.println(" does not exit."); 248 } 249 return new int[]{}; 250 } 251 return new int[]{sourceUserId}; 252 } 253 254 /** 255 * Returns a list of enabled IME IDs to address Bug 261723412. 256 * 257 * <p>This is a temporary workaround until we come up with a better solution. Do not use this 258 * for anything other than Bug 261723412.</p> 259 * 260 * @param context {@link Context} object to query secure settings. 261 * @param userId User ID to query about. 262 * @return A list of enabled IME IDs. 263 */ 264 @NonNull getEnabledInputMethodIdsForFiltering(@onNull Context context, @UserIdInt int userId)265 static List<String> getEnabledInputMethodIdsForFiltering(@NonNull Context context, 266 @UserIdInt int userId) { 267 final String enabledInputMethodsStr = TextUtils.nullIfEmpty( 268 SecureSettingsWrapper.getString(Settings.Secure.ENABLED_INPUT_METHODS, null, 269 userId)); 270 final ArrayList<String> result = new ArrayList<>(); 271 splitEnabledImeStr(enabledInputMethodsStr, result::add); 272 return result; 273 } 274 275 /** 276 * Split enabled IME string ({@link Settings.Secure#ENABLED_INPUT_METHODS}) into IME IDs. 277 * 278 * @param text a text formatted with {@link Settings.Secure#ENABLED_INPUT_METHODS}. 279 * @param consumer {@link Consumer} called back when a new IME ID is found. 280 */ 281 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) splitEnabledImeStr(@ullable String text, @NonNull Consumer<String> consumer)282 static void splitEnabledImeStr(@Nullable String text, @NonNull Consumer<String> consumer) { 283 if (TextUtils.isEmpty(text)) { 284 return; 285 } 286 final TextUtils.SimpleStringSplitter inputMethodSplitter = 287 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR); 288 final TextUtils.SimpleStringSplitter subtypeSplitter = 289 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR); 290 inputMethodSplitter.setString(text); 291 while (inputMethodSplitter.hasNext()) { 292 String nextImsStr = inputMethodSplitter.next(); 293 subtypeSplitter.setString(nextImsStr); 294 if (subtypeSplitter.hasNext()) { 295 // The first element is ime id. 296 consumer.accept(subtypeSplitter.next()); 297 } 298 } 299 } 300 301 /** 302 * Concat given IME IDs with an existing enabled IME 303 * ({@link Settings.Secure#ENABLED_INPUT_METHODS}). 304 * 305 * @param existingEnabledImeId an existing {@link Settings.Secure#ENABLED_INPUT_METHODS} to 306 * which {@code imeIDs} will be added. 307 * @param imeIds an array of IME IDs to be added. For IME IDs that are already seen in 308 * {@code existingEnabledImeId} will be skipped. 309 * @return a new enabled IME ID string that can be stored in 310 * {@link Settings.Secure#ENABLED_INPUT_METHODS}. 311 */ 312 @NonNull concatEnabledImeIds(@onNull String existingEnabledImeId, @NonNull String... imeIds)313 static String concatEnabledImeIds(@NonNull String existingEnabledImeId, 314 @NonNull String... imeIds) { 315 final ArraySet<String> alreadyEnabledIds = new ArraySet<>(); 316 final StringJoiner joiner = new StringJoiner(Character.toString(INPUT_METHOD_SEPARATOR)); 317 if (!TextUtils.isEmpty(existingEnabledImeId)) { 318 splitEnabledImeStr(existingEnabledImeId, alreadyEnabledIds::add); 319 joiner.add(existingEnabledImeId); 320 } 321 for (String id : imeIds) { 322 if (!alreadyEnabledIds.contains(id)) { 323 joiner.add(id); 324 alreadyEnabledIds.add(id); 325 } 326 } 327 return joiner.toString(); 328 } 329 330 /** 331 * Convert the input method ID to a component name 332 * 333 * @param id A unique ID for this input method. 334 * @return The component name of the input method. 335 * @see InputMethodInfo#computeId(ResolveInfo) 336 */ 337 @Nullable convertIdToComponentName(@onNull String id)338 public static ComponentName convertIdToComponentName(@NonNull String id) { 339 return ComponentName.unflattenFromString(id); 340 } 341 } 342