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
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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>