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