Bug 917480 - Part 6: tablet mode. r=nalexander
authorRichard Newman <rnewman@mozilla.com>
Tue, 13 May 2014 20:50:28 -0700
changeset 183071 2d5ab443d3c0
parent 183070 7cf6451e1c88
child 183072 a2a09fc0c39d
push id26779
push usercbook@mozilla.com
push date2014-05-14 11:02 +0000
treeherdermozilla-central@5754fc307237 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnalexander
bugs917480
milestone32.0a1
Bug 917480 - Part 6: tablet mode. r=nalexander
mobile/android/base/BrowserApp.java
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/preferences/GeckoPreferenceFragment.java
mobile/android/base/preferences/GeckoPreferences.java
mobile/android/base/preferences/LocaleListPreference.java
mobile/android/base/resources/xml-v11/preference_headers.xml
mobile/android/base/strings.xml.in
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -1783,17 +1783,20 @@ abstract public class BrowserApp extends
                 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);
+                    Log.d(LOGTAG, "Read persisted locale " + locale);
+                    if (locale == null) {
+                        return;
+                    }
                     onLocaleChanged(BrowserLocaleManager.getLanguageTag(locale));
                 }
             });
 
             return;
         default:
             return;
         }
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -116,16 +116,17 @@
 <!ENTITY pref_home_updates_enabled "Enabled">
 <!ENTITY pref_home_updates_wifi "Only over Wi-Fi">
 
 <!-- Localization note: These are shown in the left sidebar on tablets -->
 <!ENTITY pref_header_customize "Customize">
 <!ENTITY pref_header_display "Display">
 <!ENTITY pref_header_privacy_short "Privacy">
 <!ENTITY pref_header_help "Help">
+<!ENTITY pref_header_language "Language">
 <!ENTITY pref_header_vendor "&vendorShortName;">
 <!ENTITY pref_header_devtools "Developer tools">
 
 <!ENTITY pref_cookies_menu "Cookies">
 <!ENTITY pref_cookies_accept_all "Enabled">
 <!ENTITY pref_cookies_not_accept_foreign "Enabled, excluding 3rd party">
 <!ENTITY pref_cookies_disabled "Disabled">
 
--- a/mobile/android/base/preferences/GeckoPreferenceFragment.java
+++ b/mobile/android/base/preferences/GeckoPreferenceFragment.java
@@ -59,42 +59,59 @@ public class GeckoPreferenceFragment ext
     }
 
     /**
      * 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.
+     *
+     * This method sets the title that you see on non-multi-pane devices.
      */
-    protected String getTitle() {
+    private 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);
         }
 
+        // We need this because we can launch straight into this category
+        // from the Data Reporting notification.
+        if (res == R.xml.preferences_vendor) {
+            return getString(R.string.pref_category_vendor);
+        }
+
         return null;
     }
 
     private void updateTitle() {
         final String newTitle = getTitle();
-        if (newTitle != null) {
-            final Activity activity = getActivity();
+        if (newTitle == null) {
+            Log.d(LOGTAG, "No new title to show.");
+            return;
+        }
 
-            Log.v(LOGTAG, "Setting activity title to " + newTitle);
-            activity.setTitle(newTitle);
+        final PreferenceActivity activity = (PreferenceActivity) getActivity();
+        if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) && activity.isMultiPane()) {
+            // In a multi-pane activity, the title is "Settings", and the action
+            // bar is along the top of the screen. We don't want to change those.
+            activity.showBreadCrumbs(newTitle, newTitle);
+            return;
+        }
 
-            if (Build.VERSION.SDK_INT >= 14) {
-                final ActionBar actionBar = activity.getActionBar();
-                actionBar.setTitle(newTitle);
-            }
+        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();
 
@@ -133,17 +150,18 @@ public class GeckoPreferenceFragment ext
             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) activity).onIsMultiPane();
+            boolean isMultiPane = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) &&
+                                  ((PreferenceActivity) activity).isMultiPane();
             resid = isMultiPane ? R.xml.preferences_customize_tablet : R.xml.preferences;
         }
 
         return resid;
     }
 
     @Override
     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
--- a/mobile/android/base/preferences/GeckoPreferences.java
+++ b/mobile/android/base/preferences/GeckoPreferences.java
@@ -33,21 +33,23 @@ import org.mozilla.gecko.home.HomePanelP
 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;
 import android.app.Fragment;
+import android.app.FragmentManager;
 import android.app.NotificationManager;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
 import android.os.Build;
 import android.os.Bundle;
 import android.preference.CheckBoxPreference;
 import android.preference.EditTextPreference;
 import android.preference.ListPreference;
 import android.preference.Preference;
 import android.preference.Preference.OnPreferenceChangeListener;
 import android.preference.Preference.OnPreferenceClickListener;
@@ -65,18 +67,22 @@ import android.view.View;
 import android.widget.AdapterView;
 import android.widget.EditText;
 import android.widget.LinearLayout;
 import android.widget.ListAdapter;
 import android.widget.ListView;
 import android.widget.Toast;
 
 public class GeckoPreferences
-    extends PreferenceActivity
-    implements OnPreferenceChangeListener, GeckoEventListener, GeckoActivityStatus
+extends PreferenceActivity
+implements
+GeckoActivityStatus,
+GeckoEventListener,
+OnPreferenceChangeListener,
+OnSharedPreferenceChangeListener
 {
     private static final String LOGTAG = "GeckoPreferences";
 
     private static final String NON_PREF_PREFIX = "android.not_a_preference.";
     public static final String INTENT_EXTRA_RESOURCES = "resource";
     public static String PREFS_HEALTHREPORT_UPLOAD_ENABLED = NON_PREF_PREFIX + "healthreport.uploadEnabled";
 
     private static boolean sIsCharEncodingEnabled = false;
@@ -115,68 +121,140 @@ public class GeckoPreferences
     // 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.
+    private void updateActionBarTitle(int title) {
+        if (Build.VERSION.SDK_INT >= 14) {
+            final String newTitle = getString(title);
+            if (newTitle != null) {
+                Log.v(LOGTAG, "Setting action bar title to " + newTitle);
 
-        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 updateTitle(String newTitle) {
+        if (newTitle != null) {
+            Log.v(LOGTAG, "Setting activity title to " + newTitle);
+            setTitle(newTitle);
+        }
+    }
+
+    private void updateTitle(int title) {
+        updateTitle(getString(title));
+    }
+
+    /**
+     * This updates the title shown above the prefs fragment in
+     * a multi-pane view.
+     */
+    private void updateBreadcrumbTitle(int title) {
+        final String newTitle = getString(title);
+        showBreadCrumbs(newTitle, 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);
+        // If we're a multi-pane view, the activity title is really
+        // the header bar above the fragment.
+        // Find out which fragment we're showing, and use that.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && isMultiPane()) {
+            int title = getIntent().getIntExtra(EXTRA_SHOW_FRAGMENT_TITLE, -1);
+            if (res == R.xml.preferences) {
+                // This should only occur when res == R.xml.preferences,
+                // but showing "Settings" is better than crashing or showing
+                // "Fennec".
+                updateActionBarTitle(R.string.settings_title);
+            }
+
+            updateTitle(title);
+            updateBreadcrumbTitle(title);
             return;
         }
 
-        if (res == R.xml.preferences_locale) {
-            updateTitle(R.string.pref_category_language);
-            return;
+        // At present we only need to do this for non-leaf prefs views
+        // and the locale switcher itself.
+        int title = -1;
+        if (res == R.xml.preferences) {
+            title = R.string.settings_title;
+        } else if (res == R.xml.preferences_locale) {
+            title = R.string.pref_category_language;
+        } else if (res == R.xml.preferences_vendor) {
+            title = R.string.pref_category_vendor;
+        } else if (res == R.xml.preferences_customize) {
+            title = R.string.pref_category_customize;
+        }
+        if (title != -1) {
+            updateTitle(title);
         }
     }
 
     private void onLocaleChanged(Locale newLocale) {
         Log.d(LOGTAG, "onLocaleChanged: " + newLocale);
 
         BrowserLocaleManager.getInstance().updateConfiguration(getApplicationContext(), newLocale);
         this.lastLocale = newLocale;
 
+        if (Build.VERSION.SDK_INT >= 11 && isMultiPane()) {
+            // This takes care of the left pane.
+            invalidateHeaders();
+
+            // Detach and reattach the current prefs pane so that it
+            // reflects the new locale.
+            final FragmentManager fragmentManager = getFragmentManager();
+            int id = getResources().getIdentifier("android:id/prefs", null, null);
+            final Fragment current = fragmentManager.findFragmentById(id);
+            if (current != null) {
+                fragmentManager.beginTransaction()
+                               .disallowAddToBackStack()
+                               .detach(current)
+                               .attach(current)
+                               .commitAllowingStateLoss();
+            } else {
+                Log.e(LOGTAG, "No prefs fragment to reattach!");
+            }
+
+            // Because Android just rebuilt the activity itself with the
+            // old language, we need to update the top title and other
+            // wording again.
+            if (onIsMultiPane()) {
+                updateActionBarTitle(R.string.settings_title);
+            }
+
+            updateTitle(R.string.pref_header_language);
+            updateBreadcrumbTitle(R.string.pref_header_language);
+
+            // Don't finish the activity -- we just reloaded all of the
+            // individual parts! -- but when it returns, make sure that the
+            // caller knows the locale changed.
+            setResult(RESULT_CODE_LOCALE_DID_CHANGE);
+            return;
+        }
+
         // 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();
+        Log.v(LOGTAG, "Checking locale: " + currentLocale + " vs " + lastLocale);
         if (currentLocale.equals(lastLocale)) {
             return;
         }
 
         onLocaleChanged(currentLocale);
     }
 
     @Override
@@ -184,20 +262,31 @@ public class GeckoPreferences
         // 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
         // (or set it) before super.onCreate() is called so Android can display
         // the correct Fragment resource.
 
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB &&
-            !getIntent().hasExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT)) {
-            // Set up the default fragment if there is no explicit fragment to show.
-            setupTopLevelFragmentIntent();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            if (!getIntent().hasExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT)) {
+                // Set up the default fragment if there is no explicit fragment to show.
+                setupTopLevelFragmentIntent();
+
+                // This is the default header, because it's the first one.
+                // I know, this is an affront to all human decency. And yet.
+                updateTitle(getString(R.string.pref_header_customize));
+            }
+
+            // So that Android doesn't put the fragment title (or nothing at
+            // all) in the action bar.
+            if (onIsMultiPane()) {
+                updateActionBarTitle(R.string.settings_title);
+            }
         }
 
         super.onCreate(savedInstanceState);
 
         // Use setResourceToOpen to specify these extras.
         Bundle intentExtras = getIntent().getExtras();
 
         // For versions of Android lower than Honeycomb, use xml resources instead of
@@ -287,25 +376,23 @@ public class GeckoPreferences
             } else {
                 fragmentArgs.putString(INTENT_EXTRA_RESOURCES, "preferences_customize_tablet");
             }
         }
 
         // 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
     public void onBuildHeaders(List<Header> target) {
-        if (onIsMultiPane())
+        if (onIsMultiPane()) {
             loadHeadersFromResource(R.xml.preference_headers, target);
+        }
     }
 
     @Override
     public void onWindowFocusChanged(boolean hasFocus) {
         if (!hasFocus || mInitialized)
             return;
 
         mInitialized = true;
@@ -322,30 +409,48 @@ public class GeckoPreferences
             "Sanitize:Finished");
         if (mPrefsRequestId > 0) {
             PrefsHelper.removeObserver(mPrefsRequestId);
         }
     }
 
     @Override
     public void onPause() {
+        // Symmetric with onResume.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            if (isMultiPane()) {
+                SharedPreferences prefs = GeckoSharedPrefs.forApp(this);
+                prefs.unregisterOnSharedPreferenceChangeListener(this);
+            }
+        }
+
         super.onPause();
 
         if (getApplication() instanceof GeckoApplication) {
             ((GeckoApplication) getApplication()).onActivityPause(this);
         }
     }
 
     @Override
     public void onResume() {
         super.onResume();
 
         if (getApplication() instanceof GeckoApplication) {
             ((GeckoApplication) getApplication()).onActivityResume(this);
         }
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            // Watch prefs, otherwise we don't reliably get told when they change.
+            // See documentation for onSharedPreferenceChange for more.
+            // Inexplicably only needed on tablet.
+            if (isMultiPane()) {
+                SharedPreferences prefs = GeckoSharedPrefs.forApp(this);
+                prefs.registerOnSharedPreferenceChangeListener(this);
+            }
+        }
     }
 
     @Override
     public void startActivity(Intent intent) {
         // For settings, we want to be able to pass results up the chain
         // of preference screens so Settings can behave as a single unit.
         // Specifically, when we open a link, we want to back out of all
         // the settings screens.
@@ -371,20 +476,24 @@ 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) {
+              switch (resultCode) {
+              case RESULT_CODE_EXIT_SETTINGS:
+                  updateActionBarTitle(R.string.settings_title);
+
                   // Pass this result up to the parent activity.
                   setResult(RESULT_CODE_EXIT_SETTINGS);
                   finish();
+                  break;
               }
               break;
 
           case HomePanelPicker.REQUEST_CODE_ADD_PANEL:
               switch (resultCode) {
                   case Activity.RESULT_OK:
                       // Panel installed, refresh panels list.
                       mPanelsPreferenceCategory.refresh();
@@ -724,17 +833,17 @@ public class GeckoPreferences
         // 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 (newValue.equals("")) {
+                if (TextUtils.isEmpty(newValue)) {
                     BrowserLocaleManager.getInstance().resetToSystemLocale(context);
                     Telemetry.sendUIEvent(TelemetryContract.Event.LOCALE_BROWSER_RESET);
                 } else {
                     if (null == localeManager.setSelectedLocale(context, newValue)) {
                         localeManager.updateConfiguration(context, Locale.getDefault());
                     }
                     Telemetry.sendUIEvent(TelemetryContract.Event.LOCALE_BROWSER_UNSELECTED, null,
                                           currentLocale == null ? "unknown" : currentLocale);
@@ -748,16 +857,35 @@ public class GeckoPreferences
                     }
                 });
             }
         });
 
         return true;
     }
 
+    /**
+     * Implementation for the {@link OnSharedPreferenceChangeListener} interface,
+     * which we use to watch changes in our prefs file.
+     *
+     * This is reliably called whenever the pref changes, which is not the case
+     * for multiple consecutive changes in the case of onPreferenceChange.
+     *
+     * Note that this listener is not always registered: we use it only on
+     * tablets, Honeycomb and up, where we'll have a multi-pane view and prefs
+     * changing multiple times.
+     */
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+        if (PREFS_BROWSER_LOCALE.equals(key)) {
+            onLocaleSelected(BrowserLocaleManager.getLanguageTag(lastLocale),
+                             sharedPreferences.getString(key, null));
+        }
+    }
+
     @Override
     public boolean onPreferenceChange(Preference preference, Object newValue) {
         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.
@@ -883,34 +1011,34 @@ public class GeckoPreferences
             case DIALOG_CREATE_MASTER_PASSWORD:
                 final EditText input1 = getTextBox(R.string.masterpassword_password);
                 final EditText input2 = getTextBox(R.string.masterpassword_confirm);
                 linearLayout.addView(input1);
                 linearLayout.addView(input2);
 
                 builder.setTitle(R.string.masterpassword_create_title)
                        .setView((View) linearLayout)
-                       .setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {  
+                       .setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {
                             @Override
                             public void onClick(DialogInterface dialog, int which) {
                                 JSONObject jsonPref = new JSONObject();
                                 try {
                                     jsonPref.put("name", PREFS_MP_ENABLED);
                                     jsonPref.put("type", "string");
                                     jsonPref.put("value", input1.getText().toString());
-                    
+
                                     GeckoEvent event = GeckoEvent.createBroadcastEvent("Preferences:Set", jsonPref.toString());
                                     GeckoAppShell.sendEventToGecko(event);
                                 } catch(Exception ex) {
                                     Log.e(LOGTAG, "Error setting master password", ex);
                                 }
                                 return;
                             }
                         })
-                        .setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {  
+                        .setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
                             @Override
                             public void onClick(DialogInterface dialog, int which) {
                                 return;
                             }
                         });
                         dialog = builder.create();
                         dialog.setOnShowListener(new DialogInterface.OnShowListener() {
                             @Override
@@ -927,23 +1055,23 @@ public class GeckoPreferences
 
                 break;
             case DIALOG_REMOVE_MASTER_PASSWORD:
                 final EditText input = getTextBox(R.string.masterpassword_password);
                 linearLayout.addView(input);
 
                 builder.setTitle(R.string.masterpassword_remove_title)
                        .setView((View) linearLayout)
-                       .setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {  
+                       .setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {
                             @Override
                             public void onClick(DialogInterface dialog, int which) {
                                 PrefsHelper.setPref(PREFS_MP_ENABLED, input.getText().toString());
                             }
                         })
-                        .setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {  
+                        .setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
                             @Override
                             public void onClick(DialogInterface dialog, int which) {
                                 return;
                             }
                         });
                         dialog = builder.create();
                         dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
                             @Override
--- a/mobile/android/base/preferences/LocaleListPreference.java
+++ b/mobile/android/base/preferences/LocaleListPreference.java
@@ -9,19 +9,25 @@ 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.text.TextUtils;
 import android.util.AttributeSet;
+import android.util.Log;
 
 public class LocaleListPreference extends ListPreference {
+    private static final String LOG_TAG = "GeckoLocaleList";
+
+    private volatile Locale entriesLocale;
+
     public LocaleListPreference(Context context) {
         this(context, null);
     }
 
     public LocaleListPreference(Context context, AttributeSet attributes) {
         super(context, attributes);
         buildList();
     }
@@ -79,20 +85,66 @@ public class LocaleListPreference extend
         for (String tag : shippingLocales) {
             descriptors[i++] = new LocaleDescriptor(tag);
         }
 
         Arrays.sort(descriptors, 0, count);
         return descriptors;
     }
 
+    @Override
+    protected void onDialogClosed(boolean positiveResult) {
+        // The superclass will take care of persistence.
+        super.onDialogClosed(positiveResult);
+
+        // Use this hook to try to fix up the environment ASAP.
+        // Do this so that the redisplayed fragment is inflated
+        // with the right locale.
+        final Locale selectedLocale = getSelectedLocale();
+        final Context context = getContext();
+        BrowserLocaleManager.getInstance().updateConfiguration(context, selectedLocale);
+    }
+
+    private Locale getSelectedLocale() {
+        final String tag = getValue();
+        if (tag == null || tag.equals("")) {
+            return Locale.getDefault();
+        }
+        return BrowserLocaleManager.parseLocaleCode(tag);
+    }
+
+    @Override
+    public CharSequence getSummary() {
+        final String value = getValue();
+
+        if (TextUtils.isEmpty(value)) {
+            return getContext().getString(R.string.locale_system_default);
+        }
+
+        // We can't trust super.getSummary() across locale changes,
+        // apparently, so let's do the same work.
+        final Locale loc = new Locale(value);
+        return loc.getDisplayName(loc);
+    }
+
     private void buildList() {
+        final Locale currentLocale = Locale.getDefault();
+        Log.d(LOG_TAG, "Building locales list. Current locale: " + currentLocale);
+
+        if (currentLocale.equals(this.entriesLocale) &&
+            getEntries() != null) {
+            Log.v(LOG_TAG, "No need to build list.");
+            return;
+        }
+
         final LocaleDescriptor[] descriptors = getShippingLocales();
         final int count = descriptors.length;
 
+        this.entriesLocale = currentLocale;
+
         // 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) {
--- a/mobile/android/base/resources/xml-v11/preference_headers.xml
+++ b/mobile/android/base/resources/xml-v11/preference_headers.xml
@@ -23,16 +23,22 @@
 
     <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
             android:title="@string/pref_header_privacy_short">
         <extra android:name="resource"
                android:value="preferences_privacy"/>
     </header>
 
     <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
+            android:title="@string/pref_header_language">
+        <extra android:name="resource"
+               android:value="preferences_locale" />
+    </header>
+
+    <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
             android:title="@string/pref_header_vendor">
         <extra android:name="resource"
                android:value="preferences_vendor"/>
     </header>
 
     <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
             android:title="@string/pref_header_devtools">
         <extra android:name="resource"
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -125,16 +125,17 @@
   <string name="pref_category_home_content_settings">&pref_category_home_content_settings;</string>
   <string name="pref_home_updates">&pref_home_updates;</string>
   <string name="pref_home_updates_enabled">&pref_home_updates_enabled;</string>
   <string name="pref_home_updates_wifi">&pref_home_updates_wifi;</string>
 
   <string name="pref_header_customize">&pref_header_customize;</string>
   <string name="pref_header_display">&pref_header_display;</string>
   <string name="pref_header_privacy_short">&pref_header_privacy_short;</string>
+  <string name="pref_header_language">&pref_header_language;</string>
   <string name="pref_header_vendor">&pref_header_vendor;</string>
   <string name="pref_header_devtools">&pref_header_devtools;</string>
 
   <string name="pref_learn_more">&pref_learn_more;</string>
 
   <string name="pref_remember_signons">&pref_remember_signons;</string>
 
   <string name="pref_cookies_menu">&pref_cookies_menu;</string>