From cf464dbd365f1be48aff9747b564273a0bdd5089 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Tue, 13 May 2014 20:50:27 -0700 Subject: [PATCH] Bug 917480 - Part 2: add 'Language' section to top-level preferences, allowing for browser locale switching. r=nalexander * * * Bug 917480 - Part 2d: compute locale list at build time. --- mobile/android/base/AndroidManifest.xml.in | 1 - mobile/android/base/BrowserApp.java | 42 ++++- mobile/android/base/BrowserLocaleManager.java | 79 +++++++- mobile/android/base/GeckoApp.java | 38 ++-- mobile/android/base/LocaleManager.java | 1 + .../base/locales/en-US/android_strings.dtd | 12 ++ mobile/android/base/moz.build | 1 + .../preferences/GeckoPreferenceFragment.java | 77 +++++++- .../base/preferences/GeckoPreferences.java | 171 +++++++++++++++++- .../preferences/LocaleListPreference.java | 106 +++++++++++ .../base/resources/xml-v11/preferences.xml | 5 + .../base/resources/xml/preferences.xml | 10 + .../base/resources/xml/preferences_locale.xml | 19 ++ mobile/android/base/strings.xml.in | 4 + 14 files changed, 534 insertions(+), 32 deletions(-) create mode 100644 mobile/android/base/preferences/LocaleListPreference.java create mode 100644 mobile/android/base/resources/xml/preferences_locale.xml diff --git a/mobile/android/base/AndroidManifest.xml.in b/mobile/android/base/AndroidManifest.xml.in index c7a053a6801c..54474f2ac0e7 100644 --- a/mobile/android/base/AndroidManifest.xml.in +++ b/mobile/android/base/AndroidManifest.xml.in @@ -302,7 +302,6 @@ diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index 033686ece9be..3e1dc02ad490 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -9,6 +9,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.net.URLEncoder; import java.util.EnumSet; +import java.util.Locale; import java.util.Vector; import org.json.JSONArray; @@ -132,6 +133,10 @@ abstract public class BrowserApp extends GeckoApp private static final String STATE_ABOUT_HOME_TOP_PADDING = "abouthome_top_padding"; private static final String BROWSER_SEARCH_TAG = "browser_search"; + + // Request ID for startActivityForResult. + private static final int ACTIVITY_REQUEST_PREFERENCES = 1001; + private BrowserSearch mBrowserSearch; private View mBrowserSearchContainer; @@ -1288,7 +1293,7 @@ abstract public class BrowserApp extends GeckoApp } Intent settingsIntent = new Intent(this, GeckoPreferences.class); GeckoPreferences.setResourceToOpen(settingsIntent, resource); - startActivity(settingsIntent); + startActivityForResult(settingsIntent, ACTIVITY_REQUEST_PREFERENCES); } else if (event.equals("Updater:Launch")) { handleUpdaterLaunch(); } else if (event.equals("Prompt:ShowTop")) { @@ -1754,6 +1759,7 @@ abstract public class BrowserApp extends GeckoApp @Override public void onLocaleReady(final String locale) { + Log.d(LOGTAG, "onLocaleReady: " + locale); super.onLocaleReady(locale); HomePanelsManager.getInstance().onLocaleReady(locale); @@ -1764,6 +1770,35 @@ abstract public class BrowserApp extends GeckoApp } } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + Log.d(LOGTAG, "onActivityResult: " + requestCode + ", " + resultCode + ", " + data); + switch (requestCode) { + case ACTIVITY_REQUEST_PREFERENCES: + // We just returned from preferences. If our locale changed, + // we need to redisplay at this point, and do any other browser-level + // bookkeeping that we associate with a locale change. + if (resultCode != GeckoPreferences.RESULT_CODE_LOCALE_DID_CHANGE) { + Log.d(LOGTAG, "No locale change returning from preferences; nothing to do."); + return; + } + + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + public void run() { + final LocaleManager localeManager = BrowserLocaleManager.getInstance(); + final Locale locale = localeManager.getCurrentLocale(getApplicationContext()); + Log.d(LOGTAG, "Persisted locale was " + locale); + onLocaleChanged(BrowserLocaleManager.getLanguageTag(locale)); + } + }); + + return; + default: + return; + } + } + private void showHomePager(String panelId) { showHomePagerWithAnimator(panelId, null); } @@ -2420,7 +2455,10 @@ abstract public class BrowserApp extends GeckoApp if (itemId == R.id.settings) { intent = new Intent(this, GeckoPreferences.class); - startActivity(intent); + + // We want to know when the Settings activity returns, because + // we might need to redisplay based on a locale change. + startActivityForResult(intent, ACTIVITY_REQUEST_PREFERENCES); return true; } diff --git a/mobile/android/base/BrowserLocaleManager.java b/mobile/android/base/BrowserLocaleManager.java index ec39dd3fda0f..c902cadfd1c3 100644 --- a/mobile/android/base/BrowserLocaleManager.java +++ b/mobile/android/base/BrowserLocaleManager.java @@ -14,10 +14,19 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.util.Log; +import java.io.File; +import java.util.Collection; +import java.util.HashSet; import java.util.Locale; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.util.GeckoJarReader; + /** * This class manages persistence, application, and otherwise handling of * user-specified locales. @@ -39,6 +48,8 @@ public class BrowserLocaleManager implements LocaleManager { private static final String EVENT_LOCALE_CHANGED = "Locale:Changed"; private static final String PREF_LOCALE = "locale"; + private static final String FALLBACK_LOCALE_TAG = "en-US"; + // This is volatile because we don't impose restrictions // over which thread calls our methods. private volatile Locale currentLocale = null; @@ -92,7 +103,7 @@ public class BrowserLocaleManager implements LocaleManager { return language + "-" + country; } - private static Locale parseLocaleCode(final String localeCode) { + public static Locale parseLocaleCode(final String localeCode) { int index; if ((index = localeCode.indexOf('-')) != -1 || (index = localeCode.indexOf('_')) != -1) { @@ -244,7 +255,8 @@ public class BrowserLocaleManager implements LocaleManager { settings.edit().putString(PREF_LOCALE, localeCode).commit(); } - private Locale getCurrentLocale(Context context) { + @Override + public Locale getCurrentLocale(Context context) { if (currentLocale != null) { return currentLocale; } @@ -285,4 +297,67 @@ public class BrowserLocaleManager implements LocaleManager { return locale.toString(); } + + /** + * Examines multilocale.json, returning the included list of + * locale codes. + * + * If multilocale.json is not present, returns + * null. In that case, consider {@link #getFallbackLocaleTag()}. + * + * multilocale.json currently looks like this: + * + * + * {"locales": ["en-US", "be", "ca", "cs", "da", "de", "en-GB", + * "en-ZA", "es-AR", "es-ES", "es-MX", "et", "fi", + * "fr", "ga-IE", "hu", "id", "it", "ja", "ko", + * "lt", "lv", "nb-NO", "nl", "pl", "pt-BR", + * "pt-PT", "ro", "ru", "sk", "sl", "sv-SE", "th", + * "tr", "uk", "zh-CN", "zh-TW", "en-US"]} + * + */ + public static Collection getPackagedLocaleTags(final Context context) { + final String resPath = "res/multilocale.json"; + final String apkPath = context.getPackageResourcePath(); + + final String jarURL = "jar:jar:" + new File(apkPath).toURI() + "!/" + + AppConstants.OMNIJAR_NAME + "!/" + + resPath; + + final String contents = GeckoJarReader.getText(jarURL); + if (contents == null) { + // GeckoJarReader logs and swallows exceptions. + return null; + } + + try { + final JSONObject multilocale = new JSONObject(contents); + final JSONArray locales = multilocale.getJSONArray("locales"); + if (locales == null) { + Log.e(LOG_TAG, "No 'locales' array in multilocales.json!"); + return null; + } + + final Set out = new HashSet(locales.length()); + for (int i = 0; i < locales.length(); ++i) { + // If any item in the array is invalid, this will throw, + // and the entire clause will fail, being caught below + // and returning null. + out.add(locales.getString(i)); + } + + return out; + } catch (JSONException e) { + Log.e(LOG_TAG, "Unable to parse multilocale.json.", e); + return null; + } + } + + /** + * @return the single default locale baked into this application. + * Applicable when there is no multilocale.json present. + */ + public static String getFallbackLocaleTag() { + return FALLBACK_LOCALE_TAG; + } } diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index 941fd975bce6..b2e825067b74 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -2712,18 +2712,12 @@ public abstract class GeckoApp private static final String SESSION_END_LOCALE_CHANGED = "L"; /** - * Use BrowserLocaleManager to change our persisted and current locales, - * and poke HealthRecorder to tell it of our changed state. + * This exists so that a locale can be applied in two places: when saved + * in a nested activity, and then again when we get back up to GeckoApp. + * + * GeckoApp needs to do a bunch more stuff than, say, GeckoPreferences. */ - private void setLocale(final String locale) { - if (locale == null) { - return; - } - final String resultant = BrowserLocaleManager.getInstance().setSelectedLocale(this, locale); - if (resultant == null) { - return; - } - + protected void onLocaleChanged(final String locale) { final boolean startNewSession = true; final boolean shouldRestart = false; @@ -2732,7 +2726,7 @@ public abstract class GeckoApp // with the wrong locale. final HealthRecorder rec = mHealthRecorder; if (rec != null) { - rec.onAppLocaleChanged(resultant); + rec.onAppLocaleChanged(locale); rec.onEnvironmentChanged(startNewSession, SESSION_END_LOCALE_CHANGED); } @@ -2740,7 +2734,7 @@ public abstract class GeckoApp ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { - GeckoApp.this.onLocaleReady(resultant); + GeckoApp.this.onLocaleReady(locale); } }); return; @@ -2757,6 +2751,24 @@ public abstract class GeckoApp }); } + /** + * Use BrowserLocaleManager to change our persisted and current locales, + * and poke HealthRecorder to tell it of our changed state. + */ + protected void setLocale(final String locale) { + Log.d(LOGTAG, "setLocale: " + locale); + if (locale == null) { + return; + } + + final String resultant = BrowserLocaleManager.getInstance().setSelectedLocale(this, locale); + if (resultant == null) { + return; + } + + onLocaleChanged(resultant); + } + private void setSystemUiVisible(final boolean visible) { if (Build.VERSION.SDK_INT < 14) { return; diff --git a/mobile/android/base/LocaleManager.java b/mobile/android/base/LocaleManager.java index 8d6306ba2f37..eead0585ffdd 100644 --- a/mobile/android/base/LocaleManager.java +++ b/mobile/android/base/LocaleManager.java @@ -12,6 +12,7 @@ import android.content.res.Resources; public interface LocaleManager { void initialize(Context context); + Locale getCurrentLocale(Context context); String getAndApplyPersistedLocale(Context context); void correctLocale(Context context, Resources resources, Configuration newConfig); void updateConfiguration(Context context, Locale locale); diff --git a/mobile/android/base/locales/en-US/android_strings.dtd b/mobile/android/base/locales/en-US/android_strings.dtd index 8e4a9d2cdffb..e171282d88df 100644 --- a/mobile/android/base/locales/en-US/android_strings.dtd +++ b/mobile/android/base/locales/en-US/android_strings.dtd @@ -67,6 +67,18 @@ + + + + + + + + diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index 0d92b6825368..c38a4b722cc7 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -323,6 +323,7 @@ gbjar.sources += [ 'preferences/GeckoPreferenceFragment.java', 'preferences/GeckoPreferences.java', 'preferences/LinkPreference.java', + 'preferences/LocaleListPreference.java', 'preferences/ModifiableHintPreference.java', 'preferences/MultiChoicePreference.java', 'preferences/PanelsPreference.java', diff --git a/mobile/android/base/preferences/GeckoPreferenceFragment.java b/mobile/android/base/preferences/GeckoPreferenceFragment.java index e89aa6b65bed..cd050ec0708b 100644 --- a/mobile/android/base/preferences/GeckoPreferenceFragment.java +++ b/mobile/android/base/preferences/GeckoPreferenceFragment.java @@ -6,12 +6,18 @@ package org.mozilla.gecko.preferences; import java.lang.reflect.Field; +import java.util.Locale; +import org.mozilla.gecko.BrowserLocaleManager; import org.mozilla.gecko.GeckoSharedPrefs; import org.mozilla.gecko.PrefsHelper; import org.mozilla.gecko.R; +import android.app.ActionBar; import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.os.Build; import android.os.Bundle; import android.preference.PreferenceActivity; import android.preference.PreferenceFragment; @@ -29,6 +35,7 @@ public class GeckoPreferenceFragment extends PreferenceFragment { private static final String LOGTAG = "GeckoPreferenceFragment"; private int mPrefsRequestId = 0; + private Locale lastLocale = Locale.getDefault(); @Override public void onCreate(Bundle savedInstanceState) { @@ -51,6 +58,64 @@ public class GeckoPreferenceFragment extends PreferenceFragment { mPrefsRequestId = ((GeckoPreferences)getActivity()).setupPreferences(screen); } + /** + * Return the title to use for this preference fragment. This allows + * for us to redisplay this fragment in a different locale. + * + * We only return titles for the preference screens that are in the + * flow for selecting a locale, and thus might need to be redisplayed. + */ + protected String getTitle() { + final int res = getResource(); + if (res == R.xml.preferences_locale) { + return getString(R.string.pref_category_language); + } + + if (res == R.xml.preferences) { + return getString(R.string.settings_title); + } + + return null; + } + + private void updateTitle() { + final String newTitle = getTitle(); + if (newTitle != null) { + final Activity activity = getActivity(); + + Log.v(LOGTAG, "Setting activity title to " + newTitle); + activity.setTitle(newTitle); + + if (Build.VERSION.SDK_INT >= 14) { + final ActionBar actionBar = activity.getActionBar(); + actionBar.setTitle(newTitle); + } + } + } + + @Override + public void onResume() { + final Locale currentLocale = Locale.getDefault(); + final Context context = getActivity().getApplicationContext(); + + BrowserLocaleManager.getInstance().updateConfiguration(context, currentLocale); + + if (!currentLocale.equals(lastLocale)) { + // Locales differ. Let's redisplay. + Log.d(LOGTAG, "Locale changed: " + currentLocale); + this.lastLocale = currentLocale; + + // Rebuild the list to reflect the current locale. + getPreferenceScreen().removeAll(); + addPreferencesFromResource(getResource()); + } + + // Fix the parent title regardless. + updateTitle(); + + super.onResume(); + } + /* * Get the resource from Fragment arguments and return it. * @@ -59,19 +124,21 @@ public class GeckoPreferenceFragment extends PreferenceFragment { private int getResource() { int resid = 0; - String resourceName = getArguments().getString("resource"); + final String resourceName = getArguments().getString("resource"); + final Activity activity = getActivity(); + if (resourceName != null) { // Fetch resource id by resource name. - resid = getActivity().getResources().getIdentifier(resourceName, - "xml", - getActivity().getPackageName()); + final Resources resources = activity.getResources(); + final String packageName = activity.getPackageName(); + resid = resources.getIdentifier(resourceName, "xml", packageName); } if (resid == 0) { // The resource was invalid. Use the default resource. Log.e(LOGTAG, "Failed to find resource: " + resourceName + ". Displaying default settings."); - boolean isMultiPane = ((PreferenceActivity) getActivity()).onIsMultiPane(); + boolean isMultiPane = ((PreferenceActivity) activity).onIsMultiPane(); resid = isMultiPane ? R.xml.preferences_customize_tablet : R.xml.preferences; } diff --git a/mobile/android/base/preferences/GeckoPreferences.java b/mobile/android/base/preferences/GeckoPreferences.java index 87f33ec1800c..55708c4d3e35 100644 --- a/mobile/android/base/preferences/GeckoPreferences.java +++ b/mobile/android/base/preferences/GeckoPreferences.java @@ -7,18 +7,21 @@ package org.mozilla.gecko.preferences; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import org.json.JSONObject; import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.BrowserApp; +import org.mozilla.gecko.BrowserLocaleManager; import org.mozilla.gecko.DataReportingNotification; import org.mozilla.gecko.EventDispatcher; import org.mozilla.gecko.GeckoActivityStatus; -import org.mozilla.gecko.GeckoApp; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoApplication; import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.GeckoProfile; import org.mozilla.gecko.GeckoSharedPrefs; +import org.mozilla.gecko.LocaleManager; import org.mozilla.gecko.PrefsHelper; import org.mozilla.gecko.R; import org.mozilla.gecko.background.announcements.AnnouncementsConstants; @@ -28,6 +31,7 @@ import org.mozilla.gecko.home.HomePanelPicker; import org.mozilla.gecko.util.GeckoEventListener; import org.mozilla.gecko.util.ThreadUtils; +import android.app.ActionBar; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; @@ -104,8 +108,79 @@ public class GeckoPreferences private static final int REQUEST_CODE_PREF_SCREEN = 5; private static final int RESULT_CODE_EXIT_SETTINGS = 6; + // Result code used when a locale preference changes. + // Callers can recognize this code to refresh themselves to + // accommodate a locale change. + public static final int RESULT_CODE_LOCALE_DID_CHANGE = 7; + + /** + * Track the last locale so we know whether to redisplay. + */ + private Locale lastLocale = Locale.getDefault(); + + private void updateTitle(int title) { + // Due to locale switching, we need to dynamically impose the title on + // the default preferences view. + + final String newTitle = getString(title); + if (newTitle != null) { + Log.v(LOGTAG, "Setting activity title to " + newTitle); + setTitle(newTitle); + + if (Build.VERSION.SDK_INT >= 14) { + final ActionBar actionBar = getActionBar(); + actionBar.setTitle(newTitle); + } + } + } + + private void updateTitleForPrefsResource(int res) { + // At present we only need to do this for the top-level prefs view + // and the locale switcher itself. + // The others don't allow you to change locales, and have their + // titles set in their fragment descriptors. + if (res == R.xml.preferences) { + updateTitle(R.string.settings_title); + return; + } + + if (res == R.xml.preferences_locale) { + updateTitle(R.string.pref_category_language); + return; + } + } + + private void onLocaleChanged(Locale newLocale) { + Log.d(LOGTAG, "onLocaleChanged: " + newLocale); + + BrowserLocaleManager.getInstance().updateConfiguration(getApplicationContext(), newLocale); + this.lastLocale = newLocale; + + // Cause the current fragment to redisplay, the hard way. + // This avoids nonsense with trying to reach inside fragments and force them + // to redisplay themselves. + // We also don't need to update the title. + final Intent intent = (Intent) getIntent().clone(); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + startActivityForResult(intent, REQUEST_CODE_PREF_SCREEN); + + setResult(RESULT_CODE_LOCALE_DID_CHANGE); + finish(); + } + + private void checkLocale() { + final Locale currentLocale = Locale.getDefault(); + if (currentLocale.equals(lastLocale)) { + return; + } + + onLocaleChanged(currentLocale); + } + @Override protected void onCreate(Bundle savedInstanceState) { + // Apply the current user-selected locale, if necessary. + checkLocale(); // For Android v11+ where we use Fragments (v11+ only due to bug 866352), // check that PreferenceActivity.EXTRA_SHOW_FRAGMENT has been set @@ -146,6 +221,9 @@ public class GeckoPreferences Log.e(LOGTAG, "Displaying default settings."); res = R.xml.preferences; } + + // We don't include a title in the XML, so set it here, in a locale-aware fashion. + updateTitleForPrefsResource(res); addPreferencesFromResource(res); } @@ -171,8 +249,14 @@ public class GeckoPreferences } }); - if (Build.VERSION.SDK_INT >= 14) - getActionBar().setHomeButtonEnabled(true); + if (Build.VERSION.SDK_INT >= 14) { + final ActionBar actionBar = getActionBar(); + actionBar.setHomeButtonEnabled(true); + } + + // N.B., if we ever need to redisplay the locale selection UI without + // just finishing and recreating the activity, right here we'll need to + // capture EXTRA_SHOW_FRAGMENT_TITLE from the intent and store the title ID. // If launched from notification, explicitly cancel the notification. if (intentExtras != null && intentExtras.containsKey(DataReportingNotification.ALERT_NAME_DATAREPORTING_NOTIFICATION)) { @@ -182,7 +266,8 @@ public class GeckoPreferences } /** - * Set intent to display top-level settings fragment. + * Set intent to display top-level settings fragment, + * and show the correct title. */ private void setupTopLevelFragmentIntent() { Intent intent = getIntent(); @@ -205,6 +290,9 @@ public class GeckoPreferences // Build fragment intent. intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, GeckoPreferenceFragment.class.getName()); intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, fragmentArgs); + + // Show the title for the top level. + updateTitle(R.string.settings_title); } @Override @@ -267,6 +355,8 @@ public class GeckoPreferences @Override public void startWithFragment(String fragmentName, Bundle args, Fragment resultTo, int resultRequestCode, int titleRes, int shortTitleRes) { + Log.v(LOGTAG, "Starting with fragment: " + fragmentName + ", title " + titleRes); + // Overriding because we want to use startActivityForResult for Fragment intents. Intent intent = onBuildStartFragmentIntent(fragmentName, args, titleRes, shortTitleRes); if (resultTo == null) { @@ -278,6 +368,10 @@ public class GeckoPreferences @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { + // We might have just returned from a settings activity that allows us + // to switch locales, so reflect any change that occurred. + checkLocale(); + switch (requestCode) { case REQUEST_CODE_PREF_SCREEN: if (resultCode == RESULT_CODE_EXIT_SETTINGS) { @@ -290,8 +384,8 @@ public class GeckoPreferences case HomePanelPicker.REQUEST_CODE_ADD_PANEL: switch (resultCode) { case Activity.RESULT_OK: - // Panel installed, refresh panels list. - mPanelsPreferenceCategory.refresh(); + // Panel installed, refresh panels list. + mPanelsPreferenceCategory.refresh(); break; case Activity.RESULT_CANCELED: // Dialog was cancelled, do nothing. @@ -403,7 +497,8 @@ public class GeckoPreferences return true; } }); - } else if (PREFS_RESTORE_SESSION.equals(key)) { + } else if (PREFS_RESTORE_SESSION.equals(key) || + PREFS_BROWSER_LOCALE.equals(key)) { // Set the summary string to the current entry. The summary // for other list prefs will be set in the PrefsHelper // callback, but since this pref doesn't live in Gecko, we @@ -602,16 +697,74 @@ public class GeckoPreferences return prefs.getBoolean(name, def); } + /** + * Immediately handle the user's selection of a browser locale. + * + * Earlier locale-handling code did this with centralized logic in + * GeckoApp, delegating to LocaleManager for persistence and refreshing + * the activity as necessary. + * + * We no longer handle this by sending a message to GeckoApp, for + * several reasons: + * + * * GeckoApp might not be running. Activities don't always stick around. + * A Java bridge message might not be handled. + * * We need to adapt the preferences UI to the locale ourselves. + * * The user might not hit Back (or Up) -- they might hit Home and never + * come back. + * + * We handle the case of the user returning to the browser via the + * onActivityResult mechanism: see {@link BrowserApp#onActivityResult(int, int, Intent)}. + */ + private boolean onLocaleSelected(final String newValue) { + if (newValue.equals("")) { + // TODO: reset our locale to match system. + return false; + } + + final Context context = getApplicationContext(); + + // LocaleManager operations need to occur on the background thread. + // ... but activity operations need to occur on the UI thread. So we + // have nested runnables. + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + public void run() { + final LocaleManager localeManager = BrowserLocaleManager.getInstance(); + if (null == localeManager.setSelectedLocale(context, newValue)) { + localeManager.updateConfiguration(context, Locale.getDefault()); + } + + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + onLocaleChanged(Locale.getDefault()); + } + }); + } + }); + + return true; + } + @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - String prefName = preference.getKey(); + final String prefName = preference.getKey(); if (PREFS_MP_ENABLED.equals(prefName)) { showDialog((Boolean) newValue ? DIALOG_CREATE_MASTER_PASSWORD : DIALOG_REMOVE_MASTER_PASSWORD); // We don't want the "use master password" pref to change until the // user has gone through the dialog. return false; - } else if (PREFS_MENU_CHAR_ENCODING.equals(prefName)) { + } + + if (PREFS_BROWSER_LOCALE.equals(prefName)) { + // Even though this is a list preference, we don't want to handle it + // below, so we return here. + return onLocaleSelected((String) newValue); + } + + if (PREFS_MENU_CHAR_ENCODING.equals(prefName)) { setCharEncodingState(((String) newValue).equals("true")); } else if (PREFS_ANNOUNCEMENTS_ENABLED.equals(prefName)) { // Send a broadcast intent to the product announcements service, either to start or diff --git a/mobile/android/base/preferences/LocaleListPreference.java b/mobile/android/base/preferences/LocaleListPreference.java new file mode 100644 index 000000000000..1037281506be --- /dev/null +++ b/mobile/android/base/preferences/LocaleListPreference.java @@ -0,0 +1,106 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.preferences; + +import java.text.Collator; +import java.util.Arrays; +import java.util.Collection; +import java.util.Locale; + +import org.mozilla.gecko.BrowserLocaleManager; +import org.mozilla.gecko.R; + +import android.content.Context; +import android.preference.ListPreference; +import android.util.AttributeSet; + +public class LocaleListPreference extends ListPreference { + public LocaleListPreference(Context context) { + this(context, null); + } + + public LocaleListPreference(Context context, AttributeSet attributes) { + super(context, attributes); + buildList(); + } + + private static final class LocaleDescriptor implements Comparable { + // We use Locale.US here to ensure a stable ordering of entries. + private static final Collator COLLATOR = Collator.getInstance(Locale.US); + + public final String tag; + private final String nativeName; + + public LocaleDescriptor(String tag) { + this(BrowserLocaleManager.parseLocaleCode(tag), tag); + } + + public LocaleDescriptor(Locale locale, String tag) { + this.nativeName = locale.getDisplayName(locale); + this.tag = tag; + } + + public String getTag() { + return this.tag; + } + + public String getDisplayName() { + return this.nativeName; + } + + @Override + public String toString() { + return this.nativeName; + } + + + @Override + public int compareTo(LocaleDescriptor another) { + // We sort by name, so we use Collator. + return COLLATOR.compare(this.nativeName, another.nativeName); + } + } + + private LocaleDescriptor[] getShippingLocales() { + Collection shippingLocales = BrowserLocaleManager.getPackagedLocaleTags(getContext()); + + // Future: single-locale builds should be specified, too. + if (shippingLocales == null) { + final String fallbackTag = BrowserLocaleManager.getFallbackLocaleTag(); + return new LocaleDescriptor[] { new LocaleDescriptor(fallbackTag) }; + } + + final int count = shippingLocales.size(); + final LocaleDescriptor[] descriptors = new LocaleDescriptor[count]; + + int i = 0; + for (String tag : shippingLocales) { + descriptors[i++] = new LocaleDescriptor(tag); + } + + Arrays.sort(descriptors, 0, count); + return descriptors; + } + + private void buildList() { + final LocaleDescriptor[] descriptors = getShippingLocales(); + final int count = descriptors.length; + + // We leave room for "System default". + final String[] entries = new String[count + 1]; + final String[] values = new String[count + 1]; + + entries[0] = getContext().getString(R.string.locale_system_default); + values[0] = ""; + + for (int i = 0; i < count; ++i) { + entries[i + 1] = descriptors[i].getDisplayName(); + values[i + 1] = descriptors[i].getTag(); + } + + setEntries(entries); + setEntryValues(values); + } +} diff --git a/mobile/android/base/resources/xml-v11/preferences.xml b/mobile/android/base/resources/xml-v11/preferences.xml index 87cb1e2e035d..8dffcbfc4de0 100644 --- a/mobile/android/base/resources/xml-v11/preferences.xml +++ b/mobile/android/base/resources/xml-v11/preferences.xml @@ -34,6 +34,11 @@ android:value="preferences_privacy" /> + + + diff --git a/mobile/android/base/resources/xml/preferences.xml b/mobile/android/base/resources/xml/preferences.xml index 3b89428c0be0..d0b4155b5e3b 100644 --- a/mobile/android/base/resources/xml/preferences.xml +++ b/mobile/android/base/resources/xml/preferences.xml @@ -45,6 +45,16 @@ + + + + + + + + + + + + + + diff --git a/mobile/android/base/strings.xml.in b/mobile/android/base/strings.xml.in index d54e375cb778..e651a1b7c625 100644 --- a/mobile/android/base/strings.xml.in +++ b/mobile/android/base/strings.xml.in @@ -108,6 +108,10 @@ &pref_search_restore_defaults_summary; &pref_search_hint; + &pref_category_language; + &pref_browser_locale; + &locale_system_default; + &pref_category_devtools; &pref_developer_remotedebugging;