1 /* 2 * Copyright (C) 2022 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.internal.app; 18 19 import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET; 20 import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG; 21 22 import android.app.LocaleManager; 23 import android.content.Context; 24 import android.content.pm.ApplicationInfo; 25 import android.content.pm.PackageManager; 26 import android.os.Build; 27 import android.os.LocaleList; 28 import android.os.SystemProperties; 29 import android.provider.Settings; 30 import android.util.Log; 31 import android.view.inputmethod.InputMethodInfo; 32 import android.view.inputmethod.InputMethodManager; 33 34 import com.android.internal.annotations.VisibleForTesting; 35 36 import java.util.HashMap; 37 import java.util.HashSet; 38 import java.util.List; 39 import java.util.Locale; 40 import java.util.Set; 41 import java.util.stream.Collectors; 42 43 /** The Locale data collector for per-app language. */ 44 public class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase { 45 private static final String TAG = AppLocaleCollector.class.getSimpleName(); 46 private final Context mContext; 47 private final String mAppPackageName; 48 private LocaleStore.LocaleInfo mAppCurrentLocale; 49 private Set<LocaleStore.LocaleInfo> mAllAppActiveLocales; 50 private Set<LocaleStore.LocaleInfo> mImeLocales; 51 private static final String PROP_APP_LANGUAGE_SUGGESTION = 52 "android.app.language.suggestion.enhanced"; 53 private static final boolean ENABLED = true; 54 AppLocaleCollector(Context context, String appPackageName)55 public AppLocaleCollector(Context context, String appPackageName) { 56 mContext = context; 57 mAppPackageName = appPackageName; 58 } 59 60 @VisibleForTesting getAppCurrentLocale()61 public LocaleStore.LocaleInfo getAppCurrentLocale() { 62 return LocaleStore.getAppActivatedLocaleInfo(mContext, mAppPackageName, true); 63 } 64 65 /** 66 * Get all applications' activated locales. 67 * @return A set which includes all applications' activated LocaleInfo. 68 */ 69 @VisibleForTesting getAllAppActiveLocales()70 public Set<LocaleStore.LocaleInfo> getAllAppActiveLocales() { 71 PackageManager pm = mContext.getPackageManager(); 72 LocaleManager lm = mContext.getSystemService(LocaleManager.class); 73 HashSet<LocaleStore.LocaleInfo> result = new HashSet<>(); 74 if (pm != null && lm != null) { 75 HashMap<String, LocaleStore.LocaleInfo> map = new HashMap<>(); 76 for (ApplicationInfo appInfo : pm.getInstalledApplications( 77 PackageManager.ApplicationInfoFlags.of(0))) { 78 LocaleStore.LocaleInfo localeInfo = LocaleStore.getAppActivatedLocaleInfo( 79 mContext, appInfo.packageName, false); 80 // For the locale to be added into the suggestion area, its country could not be 81 // empty. 82 if (localeInfo != null && localeInfo.getLocale().getCountry().length() > 0) { 83 map.put(localeInfo.getId(), localeInfo); 84 } 85 } 86 map.forEach((language, localeInfo) -> result.add(localeInfo)); 87 } 88 return result; 89 } 90 91 /** 92 * Get all locales that active IME supports. 93 * 94 * @return A set which includes all LocaleInfo that active IME supports. 95 */ 96 @VisibleForTesting getActiveImeLocales()97 public Set<LocaleStore.LocaleInfo> getActiveImeLocales() { 98 Set<LocaleStore.LocaleInfo> activeImeLocales = null; 99 InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 100 if (imm != null) { 101 InputMethodInfo activeIme = getActiveIme(imm); 102 if (activeIme != null) { 103 activeImeLocales = LocaleStore.transformImeLanguageTagToLocaleInfo( 104 imm.getEnabledInputMethodSubtypeList(activeIme, true)); 105 } 106 } 107 if (activeImeLocales == null) { 108 return Set.of(); 109 } else { 110 return activeImeLocales.stream().filter( 111 // For the locale to be added into the suggestion area, its country could not be 112 // empty. 113 info -> info.getLocale().getCountry().length() > 0).collect( 114 Collectors.toSet()); 115 } 116 } 117 getActiveIme(InputMethodManager imm)118 private InputMethodInfo getActiveIme(InputMethodManager imm) { 119 InputMethodInfo activeIme = null; 120 List<InputMethodInfo> infoList = imm.getEnabledInputMethodList(); 121 String imeId = Settings.Secure.getStringForUser(mContext.getContentResolver(), 122 Settings.Secure.DEFAULT_INPUT_METHOD, mContext.getUserId()); 123 if (infoList != null && imeId != null) { 124 for (InputMethodInfo method : infoList) { 125 if (method.getId().equals(imeId)) { 126 activeIme = method; 127 } 128 } 129 } 130 return activeIme; 131 } 132 133 /** 134 * Get the AppLocaleResult that the application supports. 135 * @return The AppLocaleResult that the application supports. 136 */ 137 @VisibleForTesting getAppSupportedLocales()138 public AppLocaleStore.AppLocaleResult getAppSupportedLocales() { 139 return AppLocaleStore.getAppSupportedLocales(mContext, mAppPackageName); 140 } 141 142 /** 143 * Get the locales that system supports excluding langTagsToIgnore. 144 * 145 * @param langTagsToIgnore A language set to be ignored. 146 * @param parent The parent locale. 147 * @param translatedOnly specified if is is only for translation. 148 * @return A set which includes the LocaleInfo that system supports, excluding langTagsToIgnore. 149 */ 150 @VisibleForTesting getSystemSupportedLocale(Set<String> langTagsToIgnore, LocaleStore.LocaleInfo parent, boolean translatedOnly)151 public Set<LocaleStore.LocaleInfo> getSystemSupportedLocale(Set<String> langTagsToIgnore, 152 LocaleStore.LocaleInfo parent, boolean translatedOnly) { 153 return LocaleStore.getLevelLocales(mContext, langTagsToIgnore, parent, translatedOnly); 154 } 155 156 /** 157 * Get a list of system locale that removes all extensions except for the numbering system. 158 */ 159 @VisibleForTesting getSystemCurrentLocales()160 public Set<LocaleStore.LocaleInfo> getSystemCurrentLocales() { 161 Set<LocaleStore.LocaleInfo> sysLocales = LocaleStore.getSystemCurrentLocales(); 162 return sysLocales.stream().filter( 163 // For the locale to be added into the suggestion area, its country could not be 164 // empty. 165 info -> info.getLocale().getCountry().length() > 0).collect( 166 Collectors.toSet()); 167 } 168 169 @Override getIgnoredLocaleList(boolean translatedOnly)170 public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) { 171 HashSet<String> langTagsToIgnore = new HashSet<>(); 172 173 if (mAppCurrentLocale != null) { 174 langTagsToIgnore.add(mAppCurrentLocale.getLocale().toLanguageTag()); 175 } 176 177 if (SystemProperties.getBoolean(PROP_APP_LANGUAGE_SUGGESTION, ENABLED)) { 178 // Add the locale that other App activated 179 mAllAppActiveLocales.forEach( 180 info -> langTagsToIgnore.add(info.getLocale().toLanguageTag())); 181 // Add the locale that active IME enabled 182 mImeLocales.forEach(info -> langTagsToIgnore.add(info.getLocale().toLanguageTag())); 183 } 184 185 // Add System locales 186 LocaleList systemLangList = LocaleList.getDefault(); 187 for (int i = 0; i < systemLangList.size(); i++) { 188 langTagsToIgnore.add(systemLangList.get(i).toLanguageTag()); 189 } 190 return langTagsToIgnore; 191 } 192 193 @Override getSupportedLocaleList(LocaleStore.LocaleInfo parent, boolean translatedOnly, boolean isForCountryMode)194 public Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent, 195 boolean translatedOnly, boolean isForCountryMode) { 196 if (mAppCurrentLocale == null) { 197 mAppCurrentLocale = getAppCurrentLocale(); 198 } 199 if (mAllAppActiveLocales == null) { 200 mAllAppActiveLocales = getAllAppActiveLocales(); 201 } 202 if (mImeLocales == null) { 203 mImeLocales = getActiveImeLocales(); 204 } 205 AppLocaleStore.AppLocaleResult result = getAppSupportedLocales(); 206 Set<String> langTagsToIgnore = getIgnoredLocaleList(translatedOnly); 207 Set<LocaleStore.LocaleInfo> appLocaleList = new HashSet<>(); 208 Set<LocaleStore.LocaleInfo> systemLocaleList; 209 boolean shouldShowList = 210 result.mLocaleStatus == GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG 211 || result.mLocaleStatus == GET_SUPPORTED_LANGUAGE_FROM_ASSET; 212 213 // Get system supported locale list 214 if (isForCountryMode) { 215 systemLocaleList = getSystemSupportedLocale(langTagsToIgnore, parent, translatedOnly); 216 } else { 217 systemLocaleList = getSystemSupportedLocale(langTagsToIgnore, null, translatedOnly); 218 } 219 220 // Add current app locale 221 if (mAppCurrentLocale != null && !isForCountryMode) { 222 appLocaleList.add(mAppCurrentLocale); 223 } 224 225 // Add current system language into suggestion list 226 if (!isForCountryMode) { 227 boolean isCurrentLocale, existsInApp, existsInIme; 228 // filter out the system locases that are supported by the application. 229 Set<LocaleStore.LocaleInfo> localeInfoSet = 230 filterSupportedLocales(getSystemCurrentLocales(), result.mAppSupportedLocales); 231 for (LocaleStore.LocaleInfo localeInfo : localeInfoSet) { 232 isCurrentLocale = mAppCurrentLocale != null 233 && localeInfo.getLocale().equals(mAppCurrentLocale.getLocale()); 234 // Add the system suggestion flag if the localeInfo exists in mAllAppActiveLocales 235 // and mImeLocales. 236 existsInApp = addSystemSuggestionFlag(localeInfo, mAllAppActiveLocales); 237 existsInIme = addSystemSuggestionFlag(localeInfo, mImeLocales); 238 if (!isCurrentLocale && !existsInApp && !existsInIme) { 239 appLocaleList.add(localeInfo); 240 } 241 } 242 } 243 244 // Add the languages that are included in system supported locale 245 Set<LocaleStore.LocaleInfo> suggestedSet = null; 246 if (shouldShowList) { 247 appLocaleList.addAll(filterSupportedLocales(systemLocaleList, 248 result.mAppSupportedLocales)); 249 suggestedSet = getSuggestedLocales(appLocaleList); 250 } 251 252 if (!isForCountryMode && SystemProperties.getBoolean(PROP_APP_LANGUAGE_SUGGESTION, 253 ENABLED)) { 254 // Add the language that other apps activate into the suggestion list. 255 Set<LocaleStore.LocaleInfo> localeSet = filterSupportedLocales(mAllAppActiveLocales, 256 result.mAppSupportedLocales); 257 if (suggestedSet != null) { 258 // Filter out the locale with the same language and country 259 // like zh-TW vs zh-Hant-TW. 260 localeSet = filterSameLanguageAndCountry(localeSet, suggestedSet); 261 // Add IME suggestion flag if the locale is supported by IME. 262 localeSet = addImeSuggestionFlag(localeSet); 263 } 264 appLocaleList.addAll(localeSet); 265 suggestedSet.addAll(localeSet); 266 267 // Add the language that the active IME enables into the suggestion list. 268 localeSet = filterSupportedLocales(mImeLocales, result.mAppSupportedLocales); 269 if (suggestedSet != null) { 270 localeSet = filterSameLanguageAndCountry(localeSet, suggestedSet); 271 } 272 appLocaleList.addAll(localeSet); 273 suggestedSet.addAll(localeSet); 274 } 275 276 // Add "system language" option 277 if (!isForCountryMode && shouldShowList) { 278 appLocaleList.add(LocaleStore.getSystemDefaultLocaleInfo( 279 mAppCurrentLocale == null)); 280 } 281 282 if (Build.isDebuggable()) { 283 Log.d(TAG, "App locale list: " + appLocaleList); 284 } 285 286 return appLocaleList; 287 } 288 289 @Override hasSpecificPackageName()290 public boolean hasSpecificPackageName() { 291 return true; 292 } 293 getSuggestedLocales(Set<LocaleStore.LocaleInfo> localeSet)294 private Set<LocaleStore.LocaleInfo> getSuggestedLocales(Set<LocaleStore.LocaleInfo> localeSet) { 295 return localeSet.stream().filter(localeInfo -> localeInfo.isSuggested()).collect( 296 Collectors.toSet()); 297 } 298 addSystemSuggestionFlag(LocaleStore.LocaleInfo localeInfo, Set<LocaleStore.LocaleInfo> appLocaleSet)299 private boolean addSystemSuggestionFlag(LocaleStore.LocaleInfo localeInfo, 300 Set<LocaleStore.LocaleInfo> appLocaleSet) { 301 for (LocaleStore.LocaleInfo info : appLocaleSet) { 302 if (info.getLocale().equals(localeInfo.getLocale())) { 303 info.extendSuggestionOfType( 304 LocaleStore.LocaleInfo.SUGGESTION_TYPE_SYSTEM_AVAILABLE_LANGUAGE); 305 return true; 306 } 307 } 308 return false; 309 } 310 addImeSuggestionFlag( Set<LocaleStore.LocaleInfo> localeSet)311 private Set<LocaleStore.LocaleInfo> addImeSuggestionFlag( 312 Set<LocaleStore.LocaleInfo> localeSet) { 313 for (LocaleStore.LocaleInfo localeInfo : localeSet) { 314 for (LocaleStore.LocaleInfo imeLocale : mImeLocales) { 315 if (imeLocale.getLocale().equals(localeInfo.getLocale())) { 316 localeInfo.extendSuggestionOfType( 317 LocaleStore.LocaleInfo.SUGGESTION_TYPE_IME_LANGUAGE); 318 } 319 } 320 } 321 return localeSet; 322 } 323 filterSameLanguageAndCountry( Set<LocaleStore.LocaleInfo> newLocaleList, Set<LocaleStore.LocaleInfo> existingLocaleList)324 private Set<LocaleStore.LocaleInfo> filterSameLanguageAndCountry( 325 Set<LocaleStore.LocaleInfo> newLocaleList, 326 Set<LocaleStore.LocaleInfo> existingLocaleList) { 327 Set<LocaleStore.LocaleInfo> result = new HashSet<>(newLocaleList.size()); 328 for (LocaleStore.LocaleInfo appLocaleInfo : newLocaleList) { 329 boolean same = false; 330 Locale appLocale = appLocaleInfo.getLocale(); 331 for (LocaleStore.LocaleInfo localeInfo : existingLocaleList) { 332 Locale suggested = localeInfo.getLocale(); 333 if (appLocale.getLanguage().equals(suggested.getLanguage()) 334 && appLocale.getCountry().equals(suggested.getCountry())) { 335 same = true; 336 break; 337 } 338 } 339 if (!same) { 340 result.add(appLocaleInfo); 341 } 342 } 343 return result; 344 } 345 filterSupportedLocales( Set<LocaleStore.LocaleInfo> suggestedLocales, HashSet<Locale> appSupportedLocales)346 private Set<LocaleStore.LocaleInfo> filterSupportedLocales( 347 Set<LocaleStore.LocaleInfo> suggestedLocales, 348 HashSet<Locale> appSupportedLocales) { 349 Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>(); 350 351 for (LocaleStore.LocaleInfo li : suggestedLocales) { 352 if (appSupportedLocales.contains(li.getLocale())) { 353 filteredList.add(li); 354 } else { 355 for (Locale l : appSupportedLocales) { 356 if (LocaleList.matchesLanguageAndScript(li.getLocale(), l)) { 357 filteredList.add(li); 358 break; 359 } 360 } 361 } 362 } 363 return filteredList; 364 } 365 } 366