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;
20 
21 import android.app.LocaleConfig;
22 import android.content.Context;
23 import android.content.pm.ApplicationInfo;
24 import android.content.pm.InstallSourceInfo;
25 import android.content.pm.PackageManager;
26 import android.os.LocaleList;
27 import android.util.Log;
28 
29 import java.util.HashSet;
30 import java.util.Locale;
31 import java.util.stream.Collectors;
32 
33 /**
34  * A class used to access an application's supporting locales.
35  */
36 public class AppLocaleStore {
37     private static final String TAG = AppLocaleStore.class.getSimpleName();
38 
getAppSupportedLocales( Context context, String packageName)39     public static AppLocaleResult getAppSupportedLocales(
40             Context context, String packageName) {
41         LocaleConfig localeConfig = null;
42         AppLocaleResult.LocaleStatus localeStatus = LocaleStatus.UNKNOWN_FAILURE;
43         HashSet<Locale> appSupportedLocales = new HashSet<>();
44         HashSet<Locale> assetLocale = getAssetLocales(context, packageName);
45 
46         try {
47             localeConfig = new LocaleConfig(context.createPackageContext(packageName, 0));
48         } catch (PackageManager.NameNotFoundException e) {
49             Log.w(TAG, "Can not found the package name : " + packageName + " / " + e);
50         }
51 
52         if (localeConfig != null) {
53             if (localeConfig.getStatus() == LocaleConfig.STATUS_SUCCESS) {
54                 LocaleList packageLocaleList = localeConfig.getSupportedLocales();
55                 boolean shouldFilterNotMatchingLocale = !hasInstallerInfo(context, packageName) &&
56                         isSystemApp(context, packageName);
57 
58                 Log.d(TAG, "filterNonMatchingLocale. " +
59                         ", shouldFilterNotMatchingLocale: " + shouldFilterNotMatchingLocale +
60                         ", assetLocale size: " + assetLocale.size() +
61                         ", packageLocaleList size: " + packageLocaleList.size());
62 
63                 for (int i = 0; i < packageLocaleList.size(); i++) {
64                     appSupportedLocales.add(packageLocaleList.get(i));
65                 }
66                 if (shouldFilterNotMatchingLocale) {
67                     appSupportedLocales = filterNotMatchingLocale(appSupportedLocales, assetLocale);
68                 }
69 
70                 if (appSupportedLocales.size() > 0) {
71                     localeStatus = LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG;
72                 } else {
73                     localeStatus = LocaleStatus.NO_SUPPORTED_LANGUAGE_IN_APP;
74                 }
75             } else if (localeConfig.getStatus() == LocaleConfig.STATUS_NOT_SPECIFIED) {
76                 if (assetLocale.size() > 0) {
77                     localeStatus = LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET;
78                     appSupportedLocales = assetLocale;
79                 } else {
80                     localeStatus = LocaleStatus.ASSET_LOCALE_IS_EMPTY;
81                 }
82             }
83         }
84         Log.d(TAG, "getAppSupportedLocales(). package: " + packageName
85                 + ", status: " + localeStatus
86                 + ", appSupportedLocales:" + appSupportedLocales.size());
87         return new AppLocaleResult(localeStatus, appSupportedLocales);
88     }
89 
getAssetLocales(Context context, String packageName)90     private static HashSet<Locale> getAssetLocales(Context context, String packageName) {
91         HashSet<Locale> result = new HashSet<>();
92         try {
93             PackageManager packageManager = context.getPackageManager();
94             String[] locales = packageManager.getResourcesForApplication(
95                     packageManager.getPackageInfo(packageName, PackageManager.MATCH_ALL)
96                             .applicationInfo).getAssets().getNonSystemLocales();
97             if (locales == null) {
98                 Log.i(TAG, "[" + packageName + "] locales are null.");
99             } else if (locales.length <= 0) {
100                 Log.i(TAG, "[" + packageName + "] locales length is 0.");
101             } else {
102                 for (String language : locales) {
103                     result.add(Locale.forLanguageTag(language));
104                 }
105             }
106         } catch (PackageManager.NameNotFoundException e) {
107             Log.w(TAG, "Can not found the package name : " + packageName + " / " + e);
108         }
109         return result;
110     }
111 
filterNotMatchingLocale( HashSet<Locale> appSupportedLocales, HashSet<Locale> assetLocale)112     private static HashSet<Locale> filterNotMatchingLocale(
113             HashSet<Locale> appSupportedLocales, HashSet<Locale> assetLocale) {
114         return appSupportedLocales.stream()
115                 .filter(locale -> matchLanguageInSet(locale, assetLocale))
116                 .collect(Collectors.toCollection(HashSet::new));
117     }
118 
matchLanguageInSet(Locale locale, HashSet<Locale> localesSet)119     private static boolean matchLanguageInSet(Locale locale, HashSet<Locale> localesSet) {
120         if (localesSet.contains(locale)) {
121             return true;
122         }
123         for (Locale l: localesSet) {
124             if(LocaleList.matchesLanguageAndScript(l, locale)) {
125                 return true;
126             }
127         }
128         return false;
129     }
130 
hasInstallerInfo(Context context, String packageName)131     private static boolean hasInstallerInfo(Context context, String packageName) {
132         InstallSourceInfo installSourceInfo;
133         try {
134             installSourceInfo = context.getPackageManager().getInstallSourceInfo(packageName);
135             return installSourceInfo != null;
136         } catch (PackageManager.NameNotFoundException e) {
137             Log.w(TAG, "Installer info not found for: " + packageName);
138         }
139         return false;
140     }
141 
isSystemApp(Context context, String packageName)142     private static boolean isSystemApp(Context context, String packageName) {
143         ApplicationInfo applicationInfo;
144         try {
145             applicationInfo = context.getPackageManager()
146                     .getApplicationInfoAsUser(packageName, /* flags= */ 0, context.getUserId());
147             return applicationInfo.isSystemApp();
148         } catch (PackageManager.NameNotFoundException e) {
149             Log.w(TAG, "Application info not found for: " + packageName);
150         }
151         return false;
152     }
153 
154     /**
155      * A class used to store an application's supporting locales.
156      */
157     public static class AppLocaleResult {
158         public enum LocaleStatus {
159             UNKNOWN_FAILURE,
160             NO_SUPPORTED_LANGUAGE_IN_APP,
161             ASSET_LOCALE_IS_EMPTY,
162             GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG,
163             GET_SUPPORTED_LANGUAGE_FROM_ASSET,
164         }
165 
166         LocaleStatus mLocaleStatus;
167         public HashSet<Locale> mAppSupportedLocales;
168 
AppLocaleResult(LocaleStatus localeStatus, HashSet<Locale> appSupportedLocales)169         public AppLocaleResult(LocaleStatus localeStatus, HashSet<Locale> appSupportedLocales) {
170             this.mLocaleStatus = localeStatus;
171             this.mAppSupportedLocales = appSupportedLocales;
172         }
173     }
174 }
175