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