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