1 /*
2  * Copyright (C) 2010 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 android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.ActivityManager;
22 import android.app.ActivityThread;
23 import android.app.IActivityManager;
24 import android.app.ListFragment;
25 import android.app.backup.BackupManager;
26 import android.compat.annotation.UnsupportedAppUsage;
27 import android.content.Context;
28 import android.content.res.Configuration;
29 import android.content.res.Resources;
30 import android.os.Build;
31 import android.os.Bundle;
32 import android.os.LocaleList;
33 import android.os.RemoteException;
34 import android.provider.Settings;
35 import android.sysprop.LocalizationProperties;
36 import android.util.Log;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.widget.ArrayAdapter;
41 import android.widget.ListView;
42 import android.widget.TextView;
43 
44 import com.android.internal.R;
45 
46 import java.text.Collator;
47 import java.util.ArrayList;
48 import java.util.Collections;
49 import java.util.List;
50 import java.util.Locale;
51 import java.util.function.Predicate;
52 import java.util.regex.Pattern;
53 import java.util.regex.PatternSyntaxException;
54 
55 public class LocalePicker extends ListFragment {
56     private static final String TAG = "LocalePicker";
57     private static final boolean DEBUG = false;
58     private static final String[] pseudoLocales = { "en-XA", "ar-XB" };
59 
60     public static interface LocaleSelectionListener {
61         // You can add any argument if you really need it...
onLocaleSelected(Locale locale)62         public void onLocaleSelected(Locale locale);
63     }
64 
65     LocaleSelectionListener mListener;  // default to null
66 
67     public static class LocaleInfo implements Comparable<LocaleInfo> {
68         static final Collator sCollator = Collator.getInstance();
69 
70         String label;
71         final Locale locale;
72 
LocaleInfo(String label, Locale locale)73         public LocaleInfo(String label, Locale locale) {
74             this.label = label;
75             this.locale = locale;
76         }
77 
getLabel()78         public String getLabel() {
79             return label;
80         }
81 
82         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getLocale()83         public Locale getLocale() {
84             return locale;
85         }
86 
87         @Override
toString()88         public String toString() {
89             return this.label;
90         }
91 
92         @Override
compareTo(LocaleInfo another)93         public int compareTo(LocaleInfo another) {
94             return sCollator.compare(this.label, another.label);
95         }
96     }
97 
getSystemAssetLocales()98     public static String[] getSystemAssetLocales() {
99         return Resources.getSystem().getAssets().getLocales();
100     }
101 
getSupportedLocales(Context context)102     public static String[] getSupportedLocales(Context context) {
103         if (context == null) {
104             return new String[0];
105         }
106         String[] allLocales = context.getResources().getStringArray(R.array.supported_locales);
107 
108         Predicate<String> localeFilter = getLocaleFilter();
109         if (localeFilter == null) {
110             return allLocales;
111         }
112 
113         List<String> result = new ArrayList<>(allLocales.length);
114         for (String locale : allLocales) {
115             if (localeFilter.test(locale)) {
116                 result.add(locale);
117             }
118         }
119 
120         int localeCount = result.size();
121         return (localeCount == allLocales.length) ? allLocales
122                 : result.toArray(new String[localeCount]);
123     }
124 
125     @Nullable
getLocaleFilter()126     private static Predicate<String> getLocaleFilter() {
127         try {
128             return LocalizationProperties.locale_filter()
129                     .map(filter -> Pattern.compile(filter).asPredicate())
130                     .orElse(null);
131         } catch (SecurityException e) {
132             Log.e(TAG, "Failed to read locale filter.", e);
133         } catch (PatternSyntaxException e) {
134             Log.e(TAG, "Bad locale filter format (\"" + e.getPattern() + "\"), skipping.");
135         }
136 
137         return null;
138     }
139 
getAllAssetLocales(Context context, boolean isInDeveloperMode)140     public static List<LocaleInfo> getAllAssetLocales(Context context, boolean isInDeveloperMode) {
141         final Resources resources = context.getResources();
142 
143         final String[] locales = getSystemAssetLocales();
144         List<String> localeList = new ArrayList<String>(locales.length);
145         Collections.addAll(localeList, locales);
146 
147         Collections.sort(localeList);
148         final String[] specialLocaleCodes = resources.getStringArray(R.array.special_locale_codes);
149         final String[] specialLocaleNames = resources.getStringArray(R.array.special_locale_names);
150 
151         final ArrayList<LocaleInfo> localeInfos = new ArrayList<LocaleInfo>(localeList.size());
152         for (String locale : localeList) {
153             final Locale l = Locale.forLanguageTag(locale.replace('_', '-'));
154             if (l == null || "und".equals(l.getLanguage())
155                     || l.getLanguage().isEmpty() || l.getCountry().isEmpty()) {
156                 continue;
157             }
158             // Don't show the pseudolocales unless we're in developer mode. http://b/17190407.
159             if (!isInDeveloperMode && LocaleList.isPseudoLocale(l)) {
160                 continue;
161             }
162 
163             if (localeInfos.isEmpty()) {
164                 if (DEBUG) {
165                     Log.v(TAG, "adding initial "+ toTitleCase(l.getDisplayLanguage(l)));
166                 }
167                 localeInfos.add(new LocaleInfo(toTitleCase(l.getDisplayLanguage(l)), l));
168             } else {
169                 // check previous entry:
170                 //  same lang and a country -> upgrade to full name and
171                 //    insert ours with full name
172                 //  diff lang -> insert ours with lang-only name
173                 final LocaleInfo previous = localeInfos.get(localeInfos.size() - 1);
174                 if (previous.locale.getLanguage().equals(l.getLanguage()) &&
175                         !previous.locale.getLanguage().equals("zz")) {
176                     if (DEBUG) {
177                         Log.v(TAG, "backing up and fixing " + previous.label + " to " +
178                                 getDisplayName(previous.locale, specialLocaleCodes, specialLocaleNames));
179                     }
180                     previous.label = toTitleCase(getDisplayName(
181                             previous.locale, specialLocaleCodes, specialLocaleNames));
182                     if (DEBUG) {
183                         Log.v(TAG, "  and adding "+ toTitleCase(
184                                 getDisplayName(l, specialLocaleCodes, specialLocaleNames)));
185                     }
186                     localeInfos.add(new LocaleInfo(toTitleCase(
187                             getDisplayName(l, specialLocaleCodes, specialLocaleNames)), l));
188                 } else {
189                     String displayName = toTitleCase(l.getDisplayLanguage(l));
190                     if (DEBUG) {
191                         Log.v(TAG, "adding "+displayName);
192                     }
193                     localeInfos.add(new LocaleInfo(displayName, l));
194                 }
195             }
196         }
197 
198         Collections.sort(localeInfos);
199         return localeInfos;
200     }
201 
202     /**
203      * Constructs an Adapter object containing Locale information. Content is sorted by
204      * {@link LocaleInfo#label}.
205      */
constructAdapter(Context context)206     public static ArrayAdapter<LocaleInfo> constructAdapter(Context context) {
207         return constructAdapter(context, R.layout.locale_picker_item, R.id.locale);
208     }
209 
constructAdapter(Context context, final int layoutId, final int fieldId)210     public static ArrayAdapter<LocaleInfo> constructAdapter(Context context,
211             final int layoutId, final int fieldId) {
212         boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(),
213                 Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
214         final List<LocaleInfo> localeInfos = getAllAssetLocales(context, isInDeveloperMode);
215 
216         final LayoutInflater inflater =
217                 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
218         return new ArrayAdapter<LocaleInfo>(context, layoutId, fieldId, localeInfos) {
219             @Override
220             public View getView(int position, View convertView, ViewGroup parent) {
221                 View view;
222                 TextView text;
223                 if (convertView == null) {
224                     view = inflater.inflate(layoutId, parent, false);
225                     text = (TextView) view.findViewById(fieldId);
226                     view.setTag(text);
227                 } else {
228                     view = convertView;
229                     text = (TextView) view.getTag();
230                 }
231                 LocaleInfo item = getItem(position);
232                 text.setText(item.toString());
233                 text.setTextLocale(item.getLocale());
234 
235                 return view;
236             }
237         };
238     }
239 
240     private static String toTitleCase(String s) {
241         if (s.length() == 0) {
242             return s;
243         }
244 
245         return Character.toUpperCase(s.charAt(0)) + s.substring(1);
246     }
247 
248     private static String getDisplayName(
249             Locale l, String[] specialLocaleCodes, String[] specialLocaleNames) {
250         String code = l.toString();
251 
252         for (int i = 0; i < specialLocaleCodes.length; i++) {
253             if (specialLocaleCodes[i].equals(code)) {
254                 return specialLocaleNames[i];
255             }
256         }
257 
258         return l.getDisplayName(l);
259     }
260 
261     @Override
262     public void onActivityCreated(final Bundle savedInstanceState) {
263         super.onActivityCreated(savedInstanceState);
264         final ArrayAdapter<LocaleInfo> adapter = constructAdapter(getActivity());
265         setListAdapter(adapter);
266     }
267 
268     public void setLocaleSelectionListener(LocaleSelectionListener listener) {
269         mListener = listener;
270     }
271 
272     @Override
273     public void onResume() {
274         super.onResume();
275         getListView().requestFocus();
276     }
277 
278     /**
279      * Each listener needs to call {@link #updateLocale(Locale)} to actually change the locale.
280      *
281      * We don't call {@link #updateLocale(Locale)} automatically, as it halt the system for
282      * a moment and some callers won't want it.
283      */
284     @Override
285     public void onListItemClick(ListView l, View v, int position, long id) {
286         if (mListener != null) {
287             final Locale locale = ((LocaleInfo)getListAdapter().getItem(position)).locale;
288             mListener.onLocaleSelected(locale);
289         }
290     }
291 
292     /**
293      * Requests the system to update the system locale. Note that the system looks halted
294      * for a while during the Locale migration, so the caller need to take care of it.
295      *
296      * @see #updateLocales(LocaleList)
297      */
298     @UnsupportedAppUsage
299     public static void updateLocale(Locale locale) {
300         updateLocales(new LocaleList(locale));
301     }
302 
303     /**
304      * Requests the system to update the list of system locales.
305      * Note that the system looks halted for a while during the Locale migration,
306      * so the caller need to take care of it.
307      */
308     @UnsupportedAppUsage
309     public static void updateLocales(LocaleList locales) {
310         if (locales != null) {
311             locales = removeExcludedLocales(locales);
312         }
313         // Note: the empty list case is covered by Configuration.setLocales().
314 
315         try {
316             final IActivityManager am = ActivityManager.getService();
317             final Configuration config = new Configuration();
318             config.setLocales(locales);
319             config.userSetLocale = true;
320 
321             am.updatePersistentConfigurationWithAttribution(config,
322                     ActivityThread.currentOpPackageName(), null);
323             // Trigger the dirty bit for the Settings Provider.
324             BackupManager.dataChanged("com.android.providers.settings");
325         } catch (RemoteException e) {
326             // Intentionally left blank
327         }
328     }
329 
330     @NonNull
331     private static LocaleList removeExcludedLocales(@NonNull LocaleList locales) {
332         Predicate<String> localeFilter = getLocaleFilter();
333         if (localeFilter == null) {
334             return locales;
335         }
336 
337         int localeCount = locales.size();
338         ArrayList<Locale> filteredLocales = new ArrayList<>(localeCount);
339         for (int i = 0; i < localeCount; ++i) {
340             Locale locale = locales.get(i);
341             if (localeFilter.test(locale.toString())) {
342                 filteredLocales.add(locale);
343             }
344         }
345 
346         return (localeCount == filteredLocales.size()) ? locales
347                 : new LocaleList(filteredLocales.toArray(new Locale[0]));
348     }
349 
350     /**
351      * Get the locale list.
352      *
353      * @return The locale list.
354      */
355     @UnsupportedAppUsage
356     public static LocaleList getLocales() {
357         try {
358             return ActivityManager.getService()
359                     .getConfiguration().getLocales();
360         } catch (RemoteException e) {
361             // If something went wrong
362             return LocaleList.getDefault();
363         }
364     }
365 }
366