merge mozilla-central to mozilla-inbound. r=merge a=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Sun, 26 Mar 2017 21:00:31 +0200
changeset 397864 6f31760f0ffae62ca715a2b74f017ac059160bda
parent 397863 c7f3f26cc004940fcb28e5e1e860ebebf323037d (current diff)
parent 397803 cc53710589fb500610495da5258b7b9221edf681 (diff)
child 397865 1e442b92ea2d7261158419799ee001452e37d327
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge, merge
milestone55.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
merge mozilla-central to mozilla-inbound. r=merge a=merge
dom/svg/SVGContentUtils.cpp
mobile/android/base/java/org/mozilla/gecko/preferences/FontSizePreference.java
mobile/android/base/resources/layout-xlarge-v11/font_size_preference.xml
mobile/android/base/resources/layout/font_size_preference.xml
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -794,20 +794,24 @@
                   let findBar = this.mTabBrowser.getFindBar(this.mTab);
 
                   // Close the Find toolbar if we're in old-style TAF mode
                   if (findBar.findMode != findBar.FIND_NORMAL) {
                     findBar.close();
                   }
                 }
 
-                // Don't clear the favicon if this onLocationChange was
-                // triggered by a pushState or a replaceState (bug 550565) or
-                // a hash change (bug 408415).
-                if (aWebProgress.isLoadingDocument && !isSameDocument) {
+                // Don't clear the favicon if this tab is in the pending
+                // state, as SessionStore will have set the icon for us even
+                // though we're pointed at an about:blank. Also don't clear it
+                // if onLocationChange was triggered by a pushState or a
+                // replaceState (bug 550565) or a hash change (bug 408415).
+                if (!this.mTab.hasAttribute("pending") &&
+                    aWebProgress.isLoadingDocument &&
+                    !isSameDocument) {
                   this.mBrowser.mIconURL = null;
                 }
 
                 let unifiedComplete = this.mTabBrowser._unifiedComplete;
                 let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;
                 if (this.mBrowser.registeredOpenURI) {
                   unifiedComplete.unregisterOpenPage(this.mBrowser.registeredOpenURI,
                                                      userContextId);
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -892,34 +892,18 @@ var SessionStoreInternal = {
         // needs to be preserved if the load doesn't succeed.
         // We also don't do this for remoteness updates, where it should not
         // be necessary.
         if (!browser.userTypedValue && uri && !data.isRemotenessUpdate &&
             !win.gInitialPages.includes(uri)) {
           browser.userTypedValue = uri;
         }
 
-        // If the page has a title, set it.
-        if (activePageData) {
-          if (activePageData.title) {
-            tab.label = activePageData.title;
-          } else if (activePageData.url != "about:blank") {
-            tab.label = activePageData.url;
-          }
-        } else if (tab.hasAttribute("customizemode")) {
-          win.gCustomizeMode.setTab(tab);
-        }
-
-        // Restore the tab icon.
-        if ("image" in tabData) {
-          // Use the serialized contentPrincipal with the new icon load.
-          let loadingPrincipal = Utils.deserializePrincipal(tabData.iconLoadingPrincipal);
-          win.gBrowser.setIcon(tab, tabData.image, loadingPrincipal);
-          TabStateCache.update(browser, { image: null, iconLoadingPrincipal: null });
-        }
+        // Update tab label and icon again after the tab history was updated.
+        this.updateTabLabelAndIcon(tab, tabData);
 
         let event = win.document.createEvent("Events");
         event.initEvent("SSTabRestoring", true, false);
         tab.dispatchEvent(event);
         break;
       }
       case "SessionStore:restoreTabContentStarted":
         if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
@@ -2592,16 +2576,61 @@ var SessionStoreInternal = {
       }
     }
 
     // Neither a tab nor a window was found, return undefined and let the caller decide what to do about it.
     return undefined;
   },
 
   /**
+   * Updates the label and icon for a <xul:tab> using the data from
+   * tabData. If the tab being updated happens to be the
+   * customization mode tab, this function will tell the window's
+   * CustomizeMode instance about it.
+   *
+   * @param tab
+   *        The <xul:tab> to update.
+   * @param tabData (optional)
+   *        The tabData to use to update the tab. If the argument is
+   *        not supplied, the data will be retrieved from the cache.
+   */
+  updateTabLabelAndIcon(tab, tabData = null) {
+    let browser = tab.linkedBrowser;
+    let win = browser.ownerGlobal;
+
+    if (!tabData) {
+      tabData = TabState.collect(tab);
+      if (!tabData) {
+        throw new Error("tabData not found for given tab");
+      }
+    }
+
+    let activePageData = tabData.entries[tabData.index - 1] || null;
+
+    // If the page has a title, set it.
+    if (activePageData) {
+      if (activePageData.title) {
+        tab.label = activePageData.title;
+      } else if (activePageData.url != "about:blank") {
+        tab.label = activePageData.url;
+      }
+    } else if (tab.hasAttribute("customizemode")) {
+      win.gCustomizeMode.setTab(tab);
+    }
+
+    // Restore the tab icon.
+    if ("image" in tabData) {
+      // Use the serialized contentPrincipal with the new icon load.
+      let loadingPrincipal = Utils.deserializePrincipal(tabData.iconLoadingPrincipal);
+      win.gBrowser.setIcon(tab, tabData.image, loadingPrincipal);
+      TabStateCache.update(browser, { image: null, iconLoadingPrincipal: null });
+    }
+  },
+
+  /**
    * Restores the session state stored in LastSession. This will attempt
    * to merge data into the current session. If a window was opened at startup
    * with pinned tab(s), then the remaining data from the previous session for
    * that window will be opened into that window. Otherwise new windows will
    * be opened.
    */
   restoreLastSession: function ssi_restoreLastSession() {
     // Use the public getter since it also checks PB mode
@@ -3458,19 +3487,27 @@ var SessionStoreInternal = {
     // If provided, set the selected tab.
     if (aSelectTab > 0 && aSelectTab <= aTabs.length) {
       tabbrowser.selectedTab = aTabs[aSelectTab - 1];
 
       // Update the window state in case we shut down without being notified.
       this._windows[aWindow.__SSi].selected = aSelectTab;
     }
 
+    // If we restore the selected tab, make sure it goes first.
+    let selectedIndex = aTabs.indexOf(tabbrowser.selectedTab);
+    if (selectedIndex > -1) {
+      this.restoreTab(tabbrowser.selectedTab, aTabData[selectedIndex]);
+    }
+
     // Restore all tabs.
     for (let t = 0; t < aTabs.length; t++) {
-      this.restoreTab(aTabs[t], aTabData[t]);
+      if (t != selectedIndex) {
+        this.restoreTab(aTabs[t], aTabData[t]);
+      }
     }
   },
 
   // Restores the given tab state for a given tab.
   restoreTab(tab, tabData, options = {}) {
     NS_ASSERT(!tab.linkedBrowser.__SS_restoreState,
               "must reset tab before calling restoreTab()");
 
@@ -3590,16 +3627,20 @@ var SessionStoreInternal = {
       iconLoadingPrincipal: tabData.iconLoadingPrincipal || null,
       userTypedValue: tabData.userTypedValue || "",
       userTypedClear: tabData.userTypedClear || 0
     });
 
     browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory",
                                             {tabData, epoch, loadArguments});
 
+    // Update tab label and icon to show something
+    // while we wait for the messages to be processed.
+    this.updateTabLabelAndIcon(tab, tabData);
+
     // Restore tab attributes.
     if ("attributes" in tabData) {
       TabAttributes.set(tab, tabData.attributes);
     }
 
     // This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but
     // it ensures each window will have its selected tab loaded.
     if (willRestoreImmediately) {
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -241,8 +241,10 @@ run-if = e10s && crashreporter
 skip-if = debug
 [browser_docshell_uuid_consistency.js]
 [browser_grouped_session_store.js]
 skip-if = !e10s # GroupedSHistory is e10s-only
 
 [browser_closed_objects_changed_notifications_tabs.js]
 [browser_closed_objects_changed_notifications_windows.js]
 [browser_duplicate_history.js]
+[browser_tabicon_after_bg_tab_crash.js]
+skip-if = !e10s # Tabs can't crash without e10s
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_tabicon_after_bg_tab_crash.js
@@ -0,0 +1,32 @@
+"use strict";
+
+const FAVICON = "data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw==";
+const PAGE_URL = `data:text/html,
+<html>
+  <head>
+    <link rel="shortcut icon" href="${FAVICON}">
+  </head>
+  <body>
+    Favicon!
+  </body>
+</html>`;
+
+/**
+ * Tests that if a background tab crashes that it doesn't
+ * lose the favicon in the tab.
+ */
+add_task(function* test_tabicon_after_bg_tab_crash() {
+  let originalTab = gBrowser.selectedTab;
+
+  yield BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: PAGE_URL,
+  }, function*(browser) {
+    Assert.equal(browser.mIconURL, FAVICON, "Favicon is correctly set.");
+    yield BrowserTestUtils.switchTab(gBrowser, originalTab);
+    yield BrowserTestUtils.crashBrowser(browser,
+                                        false /* shouldShowTabCrashPage */);
+    Assert.equal(browser.mIconURL, FAVICON,
+                 "Favicon is still set after crash.");
+  });
+});
--- a/docshell/base/nsIContentViewer.idl
+++ b/docshell/base/nsIContentViewer.idl
@@ -195,16 +195,19 @@ interface nsIContentViewer : nsISupports
   /*
   Scrolls to a given DOM content node. 
   */
   void scrollToNode(in nsIDOMNode node);
 
   /** The amount by which to scale all text. Default is 1.0. */
   attribute float textZoom;
 
+  /** The actual text zoom in effect, as modified by the system font scale. */
+  readonly attribute float effectiveTextZoom;
+
   /** The amount by which to scale all lengths. Default is 1.0. */
   attribute float fullZoom;
 
   /**
    * The value used to override devicePixelRatio and media queries dppx.
    * Default is 0.0, that means no overriding is done (only a positive value
    * is applied).
    */
--- a/dom/smil/nsSMILCSSValueType.cpp
+++ b/dom/smil/nsSMILCSSValueType.cpp
@@ -376,17 +376,17 @@ ValueFromStringHelper(nsCSSPropertyID aP
     InvertSign(aStyleAnimValue);
   }
 
   if (aPropID == eCSSProperty_font_size) {
     // Divide out text-zoom, since SVG is supposed to ignore it
     MOZ_ASSERT(aStyleAnimValue.GetUnit() == StyleAnimationValue::eUnit_Coord,
                "'font-size' value with unexpected style unit");
     aStyleAnimValue.SetCoordValue(aStyleAnimValue.GetCoordValue() /
-                                  aPresContext->TextZoom());
+                                  aPresContext->EffectiveTextZoom());
   }
   return true;
 }
 
 // static
 void
 nsSMILCSSValueType::ValueFromString(nsCSSPropertyID aPropID,
                                     Element* aTargetElement,
--- a/dom/svg/SVGContentUtils.cpp
+++ b/dom/svg/SVGContentUtils.cpp
@@ -299,18 +299,18 @@ float
 SVGContentUtils::GetFontSize(nsStyleContext *aStyleContext)
 {
   MOZ_ASSERT(aStyleContext, "NULL style context in GetFontSize");
 
   nsPresContext *presContext = aStyleContext->PresContext();
   MOZ_ASSERT(presContext, "NULL pres context in GetFontSize");
 
   nscoord fontSize = aStyleContext->StyleFont()->mSize;
-  return nsPresContext::AppUnitsToFloatCSSPixels(fontSize) / 
-         presContext->TextZoom();
+  return nsPresContext::AppUnitsToFloatCSSPixels(fontSize) /
+         presContext->EffectiveTextZoom();
 }
 
 float
 SVGContentUtils::GetFontXHeight(Element *aElement)
 {
   if (!aElement)
     return 1.0f;
 
@@ -347,17 +347,17 @@ SVGContentUtils::GetFontXHeight(nsStyleC
   if (!fontMetrics) {
     // ReportToConsole
     NS_WARNING("no FontMetrics in GetFontXHeight()");
     return 1.0f;
   }
 
   nscoord xHeight = fontMetrics->XHeight();
   return nsPresContext::AppUnitsToFloatCSSPixels(xHeight) /
-         presContext->TextZoom();
+         presContext->EffectiveTextZoom();
 }
 nsresult
 SVGContentUtils::ReportToConsole(nsIDocument* doc,
                                  const char* aWarning,
                                  const char16_t **aParams,
                                  uint32_t aParamsLength)
 {
   return nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
--- a/layout/base/PresShell.cpp
+++ b/layout/base/PresShell.cpp
@@ -802,18 +802,18 @@ nsIPresShell::nsIPresShell()
     , mNeedThrottledAnimationFlush(true)
     , mPresShellId(0)
     , mFontSizeInflationEmPerLine(0)
     , mFontSizeInflationMinTwips(0)
     , mFontSizeInflationLineThreshold(0)
     , mFontSizeInflationForceEnabled(false)
     , mFontSizeInflationDisabledInMasterProcess(false)
     , mFontSizeInflationEnabled(false)
+    , mFontSizeInflationEnabledIsDirty(false)
     , mPaintingIsFrozen(false)
-    , mFontSizeInflationEnabledIsDirty(false)
     , mIsNeverPainting(false)
     , mInFlush(false)
   {}
 
 PresShell::PresShell()
   : mCaretEnabled(false)
 #ifdef DEBUG
   , mInVerifyReflow(false)
@@ -11027,39 +11027,43 @@ PresShell::SetupFontInflation()
 
   NotifyFontSizeInflationEnabledIsDirty();
 }
 
 void
 nsIPresShell::RecomputeFontSizeInflationEnabled()
 {
   mFontSizeInflationEnabledIsDirty = false;
-
+  mFontSizeInflationEnabled = DetermineFontSizeInflationState();
+
+  HandleSystemFontScale();
+}
+
+bool
+nsIPresShell::DetermineFontSizeInflationState()
+{
   MOZ_ASSERT(mPresContext, "our pres context should not be null");
   if ((FontSizeInflationEmPerLine() == 0 &&
       FontSizeInflationMinTwips() == 0) || mPresContext->IsChrome()) {
-    mFontSizeInflationEnabled = false;
-    return;
+    return false;
   }
 
   // Force-enabling font inflation always trumps the heuristics here.
   if (!FontSizeInflationForceEnabled()) {
     if (TabChild* tab = TabChild::GetFrom(this)) {
       // We're in a child process.  Cancel inflation if we're not
       // async-pan zoomed.
       if (!tab->AsyncPanZoomEnabled()) {
-        mFontSizeInflationEnabled = false;
-        return;
+        return false;
       }
     } else if (XRE_IsParentProcess()) {
       // We're in the master process.  Cancel inflation if it's been
       // explicitly disabled.
       if (FontSizeInflationDisabledInMasterProcess()) {
-        mFontSizeInflationEnabled = false;
-        return;
+        return false;
       }
     }
   }
 
   // XXXjwir3:
   // See bug 706918, comment 23 for more information on this particular section
   // of the code. We're using "screen size" in place of the size of the content
   // area, because on mobile, these are close or equal. This will work for our
@@ -11074,49 +11078,64 @@ nsIPresShell::RecomputeFontSizeInflation
 
   // TODO:
   // Once bug 716575 has been resolved, this code should be changed so that it
   // does the right thing on all platforms.
   nsresult rv;
   nsCOMPtr<nsIScreenManager> screenMgr =
     do_GetService("@mozilla.org/gfx/screenmanager;1", &rv);
   if (!NS_SUCCEEDED(rv)) {
-    mFontSizeInflationEnabled = false;
-    return;
+    return false;
   }
 
   nsCOMPtr<nsIScreen> screen;
   screenMgr->GetPrimaryScreen(getter_AddRefs(screen));
   if (screen) {
     int32_t screenLeft, screenTop, screenWidth, screenHeight;
     screen->GetRect(&screenLeft, &screenTop, &screenWidth, &screenHeight);
 
     nsViewportInfo vInf =
       GetDocument()->GetViewportInfo(ScreenIntSize(screenWidth, screenHeight));
 
     if (vInf.GetDefaultZoom() >= CSSToScreenScale(1.0f) || vInf.IsAutoSizeEnabled()) {
-      mFontSizeInflationEnabled = false;
-      return;
-    }
-  }
-
-  mFontSizeInflationEnabled = true;
+      return false;
+    }
+  }
+
+  return true;
 }
 
 bool
 nsIPresShell::FontSizeInflationEnabled()
 {
   if (mFontSizeInflationEnabledIsDirty) {
     RecomputeFontSizeInflationEnabled();
   }
 
   return mFontSizeInflationEnabled;
 }
 
 void
+nsIPresShell::HandleSystemFontScale()
+{
+  float fontScale = nsLayoutUtils::SystemFontScale();
+  if (fontScale == 0.0f) {
+    return;
+  }
+
+  MOZ_ASSERT(mDocument && mPresContext, "our document and pres context should not be null");
+
+  if (!mFontSizeInflationEnabled && !mDocument->IsSyntheticDocument()) {
+    mPresContext->SetSystemFontScale(fontScale);
+  } else {
+    mPresContext->SetSystemFontScale(1.0f);
+  }
+}
+
+void
 PresShell::PausePainting()
 {
   if (GetPresContext()->RefreshDriver()->GetPresContext() != GetPresContext())
     return;
 
   mPaintingIsFrozen = true;
   GetPresContext()->RefreshDriver()->Freeze();
 }
--- a/layout/base/nsDocumentViewer.cpp
+++ b/layout/base/nsDocumentViewer.cpp
@@ -3012,16 +3012,25 @@ nsDocumentViewer::GetTextZoom(float* aTe
 {
   NS_ENSURE_ARG_POINTER(aTextZoom);
   nsPresContext* pc = GetPresContext();
   *aTextZoom = pc ? pc->TextZoom() : 1.0f;
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsDocumentViewer::GetEffectiveTextZoom(float* aEffectiveTextZoom)
+{
+  NS_ENSURE_ARG_POINTER(aEffectiveTextZoom);
+  nsPresContext* pc = GetPresContext();
+  *aEffectiveTextZoom = pc ? pc->EffectiveTextZoom() : 1.0f;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsDocumentViewer::SetMinFontSize(int32_t aMinFontSize)
 {
   // If we don't have a document, then we need to bail.
   if (!mDocument) {
     return NS_ERROR_FAILURE;
   }
 
   if (GetIsPrintPreview()) {
--- a/layout/base/nsIPresShell.h
+++ b/layout/base/nsIPresShell.h
@@ -1703,16 +1703,27 @@ protected:
 
   /**
    * Do computations necessary to determine if font size inflation is enabled.
    * This value is cached after computation, as the computation is somewhat
    * expensive.
    */
   void RecomputeFontSizeInflationEnabled();
 
+  /**
+   * Does the actual work of figuring out the current state of font size inflation.
+   */
+  bool DetermineFontSizeInflationState();
+
+  /**
+   * Apply the system font scale from the corresponding pref to the PresContext,
+   * taking into account the current state of font size inflation.
+   */
+  void HandleSystemFontScale();
+
   void RecordAlloc(void* aPtr) {
 #ifdef DEBUG
     MOZ_ASSERT(!mAllocatedPointers.Contains(aPtr));
     mAllocatedPointers.PutEntry(aPtr);
 #endif
   }
 
   void RecordFree(void* aPtr) {
@@ -1910,21 +1921,22 @@ protected:
   // Cached font inflation values. This is done to prevent changing of font
   // inflation until a page is reloaded.
   uint32_t mFontSizeInflationEmPerLine;
   uint32_t mFontSizeInflationMinTwips;
   uint32_t mFontSizeInflationLineThreshold;
   bool mFontSizeInflationForceEnabled;
   bool mFontSizeInflationDisabledInMasterProcess;
   bool mFontSizeInflationEnabled;
-  bool mPaintingIsFrozen;
 
   // Dirty bit indicating that mFontSizeInflationEnabled needs to be recomputed.
   bool mFontSizeInflationEnabledIsDirty;
 
+  bool mPaintingIsFrozen;
+
   // If a document belongs to an invisible DocShell, this flag must be set
   // to true, so we can avoid any paint calls for widget related to this
   // presshell.
   bool mIsNeverPainting;
 
   // Whether we're currently under a FlushPendingNotifications.
   // This is used to handle flush reentry correctly.
   bool mInFlush;
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -174,16 +174,19 @@ typedef nsStyleTransformMatrix::Transfor
 
 /* static */ uint32_t nsLayoutUtils::sFontSizeInflationEmPerLine;
 /* static */ uint32_t nsLayoutUtils::sFontSizeInflationMinTwips;
 /* static */ uint32_t nsLayoutUtils::sFontSizeInflationLineThreshold;
 /* static */ int32_t  nsLayoutUtils::sFontSizeInflationMappingIntercept;
 /* static */ uint32_t nsLayoutUtils::sFontSizeInflationMaxRatio;
 /* static */ bool nsLayoutUtils::sFontSizeInflationForceEnabled;
 /* static */ bool nsLayoutUtils::sFontSizeInflationDisabledInMasterProcess;
+/* static */ uint32_t nsLayoutUtils::sSystemFontScale;
+/* static */ uint32_t nsLayoutUtils::sZoomMaxPercent;
+/* static */ uint32_t nsLayoutUtils::sZoomMinPercent;
 /* static */ bool nsLayoutUtils::sInvalidationDebuggingIsEnabled;
 /* static */ bool nsLayoutUtils::sCSSVariablesEnabled;
 /* static */ bool nsLayoutUtils::sInterruptibleReflowEnabled;
 /* static */ bool nsLayoutUtils::sSVGTransformBoxEnabled;
 /* static */ bool nsLayoutUtils::sTextCombineUprightDigitsEnabled;
 #ifdef MOZ_STYLO
 /* static */ bool nsLayoutUtils::sStyloEnabled;
 #endif
@@ -7711,16 +7714,22 @@ nsLayoutUtils::Initialize()
   Preferences::AddUintVarCache(&sFontSizeInflationLineThreshold,
                                "font.size.inflation.lineThreshold");
   Preferences::AddIntVarCache(&sFontSizeInflationMappingIntercept,
                               "font.size.inflation.mappingIntercept");
   Preferences::AddBoolVarCache(&sFontSizeInflationForceEnabled,
                                "font.size.inflation.forceEnabled");
   Preferences::AddBoolVarCache(&sFontSizeInflationDisabledInMasterProcess,
                                "font.size.inflation.disabledInMasterProcess");
+  Preferences::AddUintVarCache(&sSystemFontScale,
+                               "font.size.systemFontScale", 100);
+  Preferences::AddUintVarCache(&sZoomMaxPercent,
+                               "zoom.maxPercent", 300);
+  Preferences::AddUintVarCache(&sZoomMinPercent,
+                               "zoom.minPercent", 30);
   Preferences::AddBoolVarCache(&sInvalidationDebuggingIsEnabled,
                                "nglayout.debug.invalidation");
   Preferences::AddBoolVarCache(&sCSSVariablesEnabled,
                                "layout.css.variables.enabled");
   Preferences::AddBoolVarCache(&sInterruptibleReflowEnabled,
                                "layout.interruptible-reflow.enabled");
   Preferences::AddBoolVarCache(&sSVGTransformBoxEnabled,
                                "svg.transform-box.enabled");
--- a/layout/base/nsLayoutUtils.h
+++ b/layout/base/nsLayoutUtils.h
@@ -2428,16 +2428,32 @@ public:
   static bool FontSizeInflationForceEnabled() {
     return sFontSizeInflationForceEnabled;
   }
 
   static bool FontSizeInflationDisabledInMasterProcess() {
     return sFontSizeInflationDisabledInMasterProcess;
   }
 
+  /**
+   * See comment above "font.size.systemFontScale" in
+   * modules/libpref/init/all.js.
+   */
+  static float SystemFontScale() {
+    return sSystemFontScale / 100.0f;
+  }
+
+  static float MaxZoom() {
+    return sZoomMaxPercent / 100.0f;
+  }
+
+  static float MinZoom() {
+    return sZoomMinPercent / 100.0f;
+  }
+
   static bool SVGTransformBoxEnabled() {
     return sSVGTransformBoxEnabled;
   }
 
   static bool TextCombineUprightDigitsEnabled() {
     return sTextCombineUprightDigitsEnabled;
   }
 
@@ -2918,16 +2934,19 @@ public:
 private:
   static uint32_t sFontSizeInflationEmPerLine;
   static uint32_t sFontSizeInflationMinTwips;
   static uint32_t sFontSizeInflationLineThreshold;
   static int32_t  sFontSizeInflationMappingIntercept;
   static uint32_t sFontSizeInflationMaxRatio;
   static bool sFontSizeInflationForceEnabled;
   static bool sFontSizeInflationDisabledInMasterProcess;
+  static uint32_t sSystemFontScale;
+  static uint32_t sZoomMaxPercent;
+  static uint32_t sZoomMinPercent;
   static bool sInvalidationDebuggingIsEnabled;
   static bool sCSSVariablesEnabled;
   static bool sInterruptibleReflowEnabled;
   static bool sSVGTransformBoxEnabled;
   static bool sTextCombineUprightDigitsEnabled;
 #ifdef MOZ_STYLO
   static bool sStyloEnabled;
 #endif
--- a/layout/base/nsPresContext.cpp
+++ b/layout/base/nsPresContext.cpp
@@ -205,17 +205,19 @@ nsPresContext::nsPresContext(nsIDocument
   : mType(aType),
     mShell(nullptr),
     mDocument(aDocument),
     mMedium(aType == eContext_Galley ? nsGkAtoms::screen : nsGkAtoms::print),
     mMediaEmulated(mMedium),
     mLinkHandler(nullptr),
     mInflationDisabledForShrinkWrap(false),
     mBaseMinFontSize(0),
+    mSystemFontScale(1.0),
     mTextZoom(1.0),
+    mEffectiveTextZoom(1.0),
     mFullZoom(1.0),
     mOverrideDPPX(0.0),
     mLastFontInflationScreenSize(gfxSize(-1.0, -1.0)),
     mCurAppUnitsPerDevPixel(0),
     mAutoQualityMinFontSizePixelsPref(0),
     mPageSize(-1, -1),
     mPageScale(0.0),
     mPPScale(1.0f),
@@ -1314,16 +1316,39 @@ nsPresContext::GetContentLanguage() cons
     return NS_Atomize(language);
     // NOTE:  This does *not* count as an explicit language; in other
     // words, it doesn't trigger language-specific hyphenation.
   }
   return nullptr;
 }
 
 void
+nsPresContext::UpdateEffectiveTextZoom()
+{
+  float newZoom = mSystemFontScale * mTextZoom;
+  float minZoom = nsLayoutUtils::MinZoom();
+  float maxZoom = nsLayoutUtils::MaxZoom();
+
+  if (newZoom < minZoom) {
+    newZoom = minZoom;
+  } else if (newZoom > maxZoom) {
+    newZoom = maxZoom;
+  }
+
+  mEffectiveTextZoom = newZoom;
+
+  if (HasCachedStyleData()) {
+    // Media queries could have changed, since we changed the meaning
+    // of 'em' units in them.
+    MediaFeatureValuesChanged(eRestyle_ForceDescendants,
+                              NS_STYLE_HINT_REFLOW);
+  }
+}
+
+void
 nsPresContext::SetFullZoom(float aZoom)
 {
   if (!mShell || mFullZoom == aZoom) {
     return;
   }
 
   // Re-fetch the view manager's window dimensions in case there's a deferred
   // resize which hasn't affected our mVisibleArea yet
--- a/layout/base/nsPresContext.h
+++ b/layout/base/nsPresContext.h
@@ -530,31 +530,64 @@ public:
   float GetPrintPreviewScale() { return mPPScale; }
   void SetPrintPreviewScale(float aScale) { mPPScale = aScale; }
 
   nsDeviceContext* DeviceContext() const { return mDeviceContext; }
   mozilla::EventStateManager* EventStateManager() { return mEventManager; }
   nsIAtom* GetLanguageFromCharset() const { return mLanguage; }
   already_AddRefed<nsIAtom> GetContentLanguage() const;
 
+  /**
+   * Get/set a text zoom factor that is applied on top of the normal text zoom
+   * set by the front-end/user.
+   */
+  float GetSystemFontScale() const { return mSystemFontScale; }
+  void SetSystemFontScale(float aFontScale) {
+    MOZ_ASSERT(aFontScale > 0.0f, "invalid font scale");
+    if (aFontScale == mSystemFontScale || IsPrintingOrPrintPreview()) {
+      return;
+    }
+
+    mSystemFontScale = aFontScale;
+    UpdateEffectiveTextZoom();
+  }
+
+  /**
+   * Get/set the text zoom factor in use.
+   * This value should be used if you're interested in the pure text zoom value
+   * controlled by the front-end, e.g. when transferring zoom levels to a new
+   * document.
+   * Code that wants to use this value for layouting and rendering purposes
+   * should consider using EffectiveTextZoom() instead, so as to take the system
+   * font scale into account as well.
+   */
   float TextZoom() const { return mTextZoom; }
   void SetTextZoom(float aZoom) {
     MOZ_ASSERT(aZoom > 0.0f, "invalid zoom factor");
     if (aZoom == mTextZoom)
       return;
 
     mTextZoom = aZoom;
-    if (HasCachedStyleData()) {
-      // Media queries could have changed, since we changed the meaning
-      // of 'em' units in them.
-      MediaFeatureValuesChanged(eRestyle_ForceDescendants,
-                                NS_STYLE_HINT_REFLOW);
-    }
+    UpdateEffectiveTextZoom();
   }
 
+protected:
+  void UpdateEffectiveTextZoom();
+
+public:
+  /**
+   * Corresponds to the product of text zoom and system font scale, limited
+   * by zoom.maxPercent and minPercent.
+   * As the system font scale is automatically set by the PresShell, code that
+   * e.g. wants to transfer zoom levels to a new document should use TextZoom()
+   * instead, which corresponds to the text zoom level that was actually set by
+   * the front-end/user.
+   */
+  float EffectiveTextZoom() const { return mEffectiveTextZoom; }
+
   /**
    * Get the minimum font size for the specified language. If aLanguage
    * is nullptr, then the document's language is used.  This combines
    * the language-specific global preference with the per-presentation
    * base minimum font size.
    */
   int32_t MinFontSize(nsIAtom *aLanguage) const {
     const LangGroupFontPrefs *prefs = GetFontPrefsForLang(aLanguage);
@@ -926,16 +959,17 @@ public:
   }
 
   gfxTextPerfMetrics *GetTextPerfMetrics() { return mTextPerf; }
 
   bool IsDynamic() { return (mType == eContext_PageLayout || mType == eContext_Galley); }
   bool IsScreen() { return (mMedium == nsGkAtoms::screen ||
                               mType == eContext_PageLayout ||
                               mType == eContext_PrintPreview); }
+  bool IsPrintingOrPrintPreview() { return (mType == eContext_Print || mType == eContext_PrintPreview); }
 
   // Is this presentation in a chrome docshell?
   bool IsChrome() const { return mIsChrome; }
   bool IsChromeOriginImage() const { return mIsChromeOriginImage; }
   void UpdateIsChrome();
 
   // Public API for native theme code to get style internals.
   bool HasAuthorSpecifiedRules(const nsIFrame *aFrame,
@@ -1295,17 +1329,19 @@ public:
   bool                  mInflationDisabledForShrinkWrap;
 
 protected:
 
   mozilla::WeakPtr<nsDocShell>             mContainer;
 
   // Base minimum font size, independent of the language-specific global preference. Defaults to 0
   int32_t               mBaseMinFontSize;
+  float                 mSystemFontScale; // Internal text zoom factor, defaults to 1.0
   float                 mTextZoom;      // Text zoom, defaults to 1.0
+  float                 mEffectiveTextZoom; // Text zoom * system font scale
   float                 mFullZoom;      // Page zoom, defaults to 1.0
   float                 mOverrideDPPX;   // DPPX overrided, defaults to 0.0
   gfxSize               mLastFontInflationScreenSize;
 
   int32_t               mCurAppUnitsPerDevPixel;
   int32_t               mAutoQualityMinFontSizePixelsPref;
 
   nsCOMPtr<nsITheme> mTheme;
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -2012,17 +2012,17 @@ nsFrame::DisplaySelectionOverlay(nsDispl
   aList->AppendNewToTop(new (aBuilder)
     nsDisplaySelectionOverlay(aBuilder, this, selectionValue));
 }
 
 void
 nsFrame::DisplayOutlineUnconditional(nsDisplayListBuilder*   aBuilder,
                                      const nsDisplayListSet& aLists)
 {
-  if (StyleOutline()->mOutlineStyle == NS_STYLE_BORDER_STYLE_NONE) {
+  if (!StyleOutline()->ShouldPaintOutline()) {
     return;
   }
 
   aLists.Outlines()->AppendNewToTop(
     new (aBuilder) nsDisplayOutline(aBuilder, this));
 }
 
 void
@@ -5617,18 +5617,18 @@ nsRect
 nsIFrame::ComputeTightBounds(DrawTarget* aDrawTarget) const
 {
   return GetVisualOverflowRect();
 }
 
 nsRect
 nsFrame::ComputeSimpleTightBounds(DrawTarget* aDrawTarget) const
 {
-  if (StyleOutline()->mOutlineStyle != NS_STYLE_BORDER_STYLE_NONE ||
-      StyleBorder()->HasBorder() || !StyleBackground()->IsTransparent(this) ||
+  if (StyleOutline()->ShouldPaintOutline() || StyleBorder()->HasBorder() ||
+      !StyleBackground()->IsTransparent(this) ||
       StyleDisplay()->UsedAppearance()) {
     // Not necessarily tight, due to clipping, negative
     // outline-offset, and lots of other issues, but that's OK
     return GetVisualOverflowRect();
   }
 
   nsRect r(0, 0, 0, 0);
   ChildListIterator lists(this);
@@ -8808,23 +8808,17 @@ UnionBorderBoxes(nsIFrame* aFrame, bool 
   return u;
 }
 
 static void
 ComputeAndIncludeOutlineArea(nsIFrame* aFrame, nsOverflowAreas& aOverflowAreas,
                              const nsSize& aNewSize)
 {
   const nsStyleOutline* outline = aFrame->StyleOutline();
-  const uint8_t outlineStyle = outline->mOutlineStyle;
-  if (outlineStyle == NS_STYLE_BORDER_STYLE_NONE) {
-    return;
-  }
-
-  nscoord width = outline->GetOutlineWidth();
-  if (width <= 0 && outlineStyle != NS_STYLE_BORDER_STYLE_AUTO) {
+  if (!outline->ShouldPaintOutline()) {
     return;
   }
 
   // When the outline property is set on :-moz-anonymous-block or
   // :-moz-anonymous-positioned-block pseudo-elements, it inherited
   // that outline from the inline that was broken because it
   // contained a block.  In that case, we don't want a really wide
   // outline if the block inside the inline is narrow, so union the
@@ -8876,31 +8870,32 @@ ComputeAndIncludeOutlineArea(nsIFrame* a
 
   // Keep this code in sync with GetOutlineInnerRect in nsCSSRendering.cpp.
   aFrame->Properties().Set(nsIFrame::OutlineInnerRectProperty(),
                            new nsRect(innerRect));
   const nscoord offset = outline->mOutlineOffset;
   nsRect outerRect(innerRect);
   bool useOutlineAuto = false;
   if (nsLayoutUtils::IsOutlineStyleAutoEnabled()) {
-    useOutlineAuto = outlineStyle == NS_STYLE_BORDER_STYLE_AUTO;
+    useOutlineAuto = outline->mOutlineStyle == NS_STYLE_BORDER_STYLE_AUTO;
     if (MOZ_UNLIKELY(useOutlineAuto)) {
       nsPresContext* presContext = aFrame->PresContext();
       nsITheme* theme = presContext->GetTheme();
       if (theme && theme->ThemeSupportsWidget(presContext, aFrame,
                                               NS_THEME_FOCUS_OUTLINE)) {
         outerRect.Inflate(offset);
         theme->GetWidgetOverflow(presContext->DeviceContext(), aFrame,
                                  NS_THEME_FOCUS_OUTLINE, &outerRect);
       } else {
         useOutlineAuto = false;
       }
     }
   }
   if (MOZ_LIKELY(!useOutlineAuto)) {
+    nscoord width = outline->GetOutlineWidth();
     outerRect.Inflate(width + offset);
   }
 
   nsRect& vo = aOverflowAreas.VisualOverflow();
   vo.UnionRectEdges(vo, innerRect.Union(outerRect));
 }
 
 bool
--- a/layout/painting/nsCSSRendering.cpp
+++ b/layout/painting/nsCSSRendering.cpp
@@ -960,23 +960,18 @@ nsCSSRendering::CreateBorderRendererForO
                                                const nsRect& aDirtyRect,
                                                const nsRect& aBorderArea,
                                                nsStyleContext* aStyleContext)
 {
   nscoord             twipsRadii[8];
 
   // Get our style context's color struct.
   const nsStyleOutline* ourOutline = aStyleContext->StyleOutline();
-  MOZ_ASSERT(ourOutline != NS_STYLE_BORDER_STYLE_NONE,
-             "shouldn't have created nsDisplayOutline item");
-
-  uint8_t outlineStyle = ourOutline->mOutlineStyle;
-  nscoord width = ourOutline->GetOutlineWidth();
-
-  if (width == 0 && outlineStyle != NS_STYLE_BORDER_STYLE_AUTO) {
+
+  if (!ourOutline->ShouldPaintOutline()) {
     // Empty outline
     return Nothing();
   }
 
   nsIFrame* bgFrame = nsCSSRendering::FindNonTransparentBackgroundFrame
     (aForFrame, false);
   nsStyleContext* bgContext = bgFrame->StyleContext();
   nscolor bgColor = bgContext->
@@ -999,16 +994,18 @@ nsCSSRendering::CreateBorderRendererForO
   // If the dirty rect is completely inside the border area (e.g., only the
   // content is being painted), then we can skip out now
   // XXX this isn't exactly true for rounded borders, where the inside curves may
   // encroach into the content area.  A safer calculation would be to
   // shorten insideRect by the radius one each side before performing this test.
   if (innerRect.Contains(aDirtyRect))
     return Nothing();
 
+  nscoord width = ourOutline->GetOutlineWidth();
+
   nsRect outerRect = innerRect;
   outerRect.Inflate(width, width);
 
   // get the radius for our outline
   nsIFrame::ComputeBorderRadii(ourOutline->mOutlineRadius, aBorderArea.Size(),
                                outerRect.Size(), Sides(), twipsRadii);
 
   // Get our conversion values
@@ -1017,16 +1014,17 @@ nsCSSRendering::CreateBorderRendererForO
   // get the outer rectangles
   Rect oRect(NSRectToRect(outerRect, twipsPerPixel));
 
   // convert the radii
   nsMargin outlineMargin(width, width, width, width);
   RectCornerRadii outlineRadii;
   ComputePixelRadii(twipsRadii, twipsPerPixel, &outlineRadii);
 
+  uint8_t outlineStyle = ourOutline->mOutlineStyle;
   if (outlineStyle == NS_STYLE_BORDER_STYLE_AUTO) {
     if (nsLayoutUtils::IsOutlineStyleAutoEnabled()) {
       nsITheme* theme = aPresContext->GetTheme();
       if (theme && theme->ThemeSupportsWidget(aPresContext, aForFrame,
                                               NS_THEME_FOCUS_OUTLINE)) {
         theme->DrawWidgetBackground(aRenderingContext, aForFrame,
                                     NS_THEME_FOCUS_OUTLINE, innerRect,
                                     aDirtyRect);
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -4072,16 +4072,19 @@ nsDisplayOutline::GetBounds(nsDisplayLis
   *aSnap = false;
   return mFrame->GetVisualOverflowRectRelativeToSelf() + ToReferenceFrame();
 }
 
 void
 nsDisplayOutline::Paint(nsDisplayListBuilder* aBuilder,
                         nsRenderingContext* aCtx) {
   // TODO join outlines together
+  MOZ_ASSERT(mFrame->StyleOutline()->ShouldPaintOutline(),
+             "Should have not created a nsDisplayOutline!");
+
   nsPoint offset = ToReferenceFrame();
   nsCSSRendering::PaintOutline(mFrame->PresContext(), *aCtx, mFrame,
                                mVisibleRect,
                                nsRect(offset, mFrame->GetSize()),
                                mFrame->StyleContext());
 }
 
 LayerState
--- a/layout/style/nsComputedDOMStyle.cpp
+++ b/layout/style/nsComputedDOMStyle.cpp
@@ -5409,17 +5409,17 @@ nsComputedDOMStyle::GetLineHeightCoord(n
                                              blockHeight, 1.0f);
 
   // CalcLineHeight uses font->mFont.size, but we want to use
   // font->mSize as the font size.  Adjust for that.  Also adjust for
   // the text zoom, if any.
   const nsStyleFont* font = StyleFont();
   float fCoord = float(aCoord);
   if (font->mAllowZoom) {
-    fCoord /= mPresShell->GetPresContext()->TextZoom();
+    fCoord /= mPresShell->GetPresContext()->EffectiveTextZoom();
   }
   if (font->mFont.size != font->mSize) {
     fCoord = fCoord * (float(font->mSize) / float(font->mFont.size));
   }
   aCoord = NSToCoordRound(fCoord);
 
   return true;
 }
--- a/layout/style/nsStyleStruct.cpp
+++ b/layout/style/nsStyleStruct.cpp
@@ -195,25 +195,25 @@ nsStyleFont::CalcDifference(const nsStyl
   return nsChangeHint(0);
 }
 
 /* static */ nscoord
 nsStyleFont::ZoomText(const nsPresContext* aPresContext, nscoord aSize)
 {
   // aSize can be negative (e.g.: calc(-1px)) so we can't assert that here.
   // The caller is expected deal with that.
-  return NSToCoordTruncClamped(float(aSize) * aPresContext->TextZoom());
+  return NSToCoordTruncClamped(float(aSize) * aPresContext->EffectiveTextZoom());
 }
 
 /* static */ nscoord
 nsStyleFont::UnZoomText(nsPresContext *aPresContext, nscoord aSize)
 {
   // aSize can be negative (e.g.: calc(-1px)) so we can't assert that here.
   // The caller is expected deal with that.
-  return NSToCoordTruncClamped(float(aSize) / aPresContext->TextZoom());
+  return NSToCoordTruncClamped(float(aSize) / aPresContext->EffectiveTextZoom());
 }
 
 /* static */ already_AddRefed<nsIAtom>
 nsStyleFont::GetLanguage(const nsPresContext* aPresContext)
 {
   RefPtr<nsIAtom> language = aPresContext->GetContentLanguage();
   if (!language) {
     // we didn't find a (usable) Content-Language, so we fall back
--- a/layout/style/nsStyleStruct.h
+++ b/layout/style/nsStyleStruct.h
@@ -1387,16 +1387,23 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsSt
   mozilla::StyleComplexColor mOutlineColor; // [reset]
   uint8_t       mOutlineStyle;    // [reset] See nsStyleConsts.h
 
   nscoord GetOutlineWidth() const
   {
     return mActualOutlineWidth;
   }
 
+  bool ShouldPaintOutline() const
+  {
+    return mOutlineStyle == NS_STYLE_BORDER_STYLE_AUTO ||
+           (GetOutlineWidth() > 0 &&
+            mOutlineStyle != NS_STYLE_BORDER_STYLE_NONE);
+  }
+
 protected:
   // The actual value of outline-width is the computed value (an absolute
   // length, forced to zero when outline-style is none) rounded to device
   // pixels.  This is the value used by layout.
   nscoord       mActualOutlineWidth;
   nscoord       mTwipsPerPixel;
 };
 
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
@@ -14,26 +14,24 @@ import android.util.Log;
 
 import com.squareup.leakcanary.LeakCanary;
 import com.squareup.leakcanary.RefWatcher;
 
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.LocalBrowserDB;
 import org.mozilla.gecko.distribution.Distribution;
-import org.mozilla.gecko.dlc.DownloadContentService;
 import org.mozilla.gecko.home.HomePanelsManager;
 import org.mozilla.gecko.lwt.LightweightTheme;
 import org.mozilla.gecko.mdns.MulticastDNSManager;
 import org.mozilla.gecko.media.AudioFocusAgent;
 import org.mozilla.gecko.notifications.NotificationClient;
 import org.mozilla.gecko.notifications.NotificationHelper;
 import org.mozilla.gecko.preferences.DistroSharedPrefsImport;
 import org.mozilla.gecko.util.BundleEventListener;
-import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.io.File;
 import java.lang.reflect.Method;
 
@@ -127,16 +125,17 @@ public class GeckoApplication extends Ap
 
             GeckoNetworkManager.getInstance().stop();
         }
     }
 
     public void onActivityResume(GeckoActivityStatus activity) {
         if (mIsInitialResume) {
             GeckoBatteryManager.getInstance().start(this);
+            GeckoFontScaleListener.getInstance().initialize(this);
             GeckoNetworkManager.getInstance().start(this);
             mIsInitialResume = false;
         } else if (mPausedGecko) {
             GeckoThread.onResume();
             mPausedGecko = false;
             GeckoNetworkManager.getInstance().start(this);
         }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoFontScaleListener.java
@@ -0,0 +1,134 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.provider.Settings;
+import android.support.annotation.UiThread;
+import android.util.Log;
+
+import org.mozilla.gecko.preferences.GeckoPreferences;
+
+class GeckoFontScaleListener
+        extends ContentObserver
+        implements SharedPreferences.OnSharedPreferenceChangeListener {
+    private static final String LOGTAG = "GeckoFontScaleListener";
+
+    private static final String PREF_SYSTEM_FONT_SCALE = "font.size.systemFontScale";
+    private static final String PREF_FONT_INFLATION = "font.size.inflation.minTwips";
+    private static final int FONT_INFLATION_OFF = 0;
+    private static final int FONT_INFLATION_ON_DEFAULT_VALUE = 120;
+    private static final float DEFAULT_FONT_SCALE = 1.0f;
+
+    private static final GeckoFontScaleListener listenerInstance = new GeckoFontScaleListener();
+
+    private Context applicationContext;
+    private boolean initialized;
+    private boolean running;
+
+    public static GeckoFontScaleListener getInstance() {
+        return listenerInstance;
+    }
+
+    private GeckoFontScaleListener() {
+        super(null);
+    }
+
+    public synchronized void initialize(final Context context) {
+        if (initialized) {
+            Log.w(LOGTAG, "Already initialized!");
+            return;
+        }
+
+        applicationContext = context.getApplicationContext();
+        SharedPreferences prefs = GeckoSharedPrefs.forApp(applicationContext);
+        prefs.registerOnSharedPreferenceChangeListener(this);
+        onPrefChange(prefs);
+        initialized = true;
+    }
+
+    public synchronized void shutdown() {
+        if (!initialized) {
+            Log.w(LOGTAG, "Already shut down!");
+            return;
+        }
+
+        GeckoSharedPrefs.forApp(applicationContext).unregisterOnSharedPreferenceChangeListener(this);
+        stop();
+        applicationContext = null;
+        initialized = false;
+    }
+
+    private synchronized void start() {
+        if (running) {
+            return;
+        }
+
+        ContentResolver contentResolver = applicationContext.getContentResolver();
+        Uri fontSizeSetting = Settings.System.getUriFor(Settings.System.FONT_SCALE);
+        contentResolver.registerContentObserver(fontSizeSetting, false, this);
+        onSystemFontScaleChange(contentResolver, false);
+
+        running = true;
+    }
+
+    private synchronized void stop() {
+        if (!running) {
+            return;
+        }
+
+        ContentResolver contentResolver = applicationContext.getContentResolver();
+        contentResolver.unregisterContentObserver(this);
+        onSystemFontScaleChange(contentResolver, /*stopping*/ true);
+
+        running = false;
+    }
+
+    private void onSystemFontScaleChange(final ContentResolver contentResolver, boolean stopping) {
+        float fontScale;
+        int fontInflation;
+
+        if (!stopping) { // Pref was flipped to "On" or system font scale changed.
+            fontScale = Settings.System.getFloat(contentResolver, Settings.System.FONT_SCALE, DEFAULT_FONT_SCALE);
+            fontInflation = Math.round(FONT_INFLATION_ON_DEFAULT_VALUE * fontScale);
+        } else { // Pref was flipped to "Off".
+            fontScale = DEFAULT_FONT_SCALE;
+            fontInflation = FONT_INFLATION_OFF;
+        }
+
+        PrefsHelper.setPref(PREF_FONT_INFLATION, fontInflation);
+        PrefsHelper.setPref(PREF_SYSTEM_FONT_SCALE, Math.round(fontScale * 100));
+    }
+
+    private void onPrefChange(final SharedPreferences prefs) {
+        boolean useSystemFontScale = prefs.getBoolean(GeckoPreferences.PREFS_SYSTEM_FONT_SIZE, false);
+
+        if (useSystemFontScale) {
+            start();
+        } else {
+            stop();
+        }
+    }
+
+    @Override
+    public void onChange(boolean selfChange) {
+        onSystemFontScaleChange(applicationContext.getContentResolver(), false);
+    }
+
+    @UiThread // According to the docs.
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+        if (!GeckoPreferences.PREFS_SYSTEM_FONT_SIZE.equals(key)) {
+            return;
+        }
+
+        onPrefChange(sharedPreferences);
+    }
+}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/FontSizePreference.java
+++ /dev/null
@@ -1,192 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * 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 org.mozilla.gecko.R;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Color;
-import android.preference.DialogPreference;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.ScrollView;
-import android.widget.TextView;
-
-import java.util.HashMap;
-
-class FontSizePreference extends DialogPreference {
-    private static final String LOGTAG = "FontSizePreference";
-    private static final int TWIP_TO_PT_RATIO = 20; // 20 twip = 1 point.
-    private static final int PREVIEW_FONT_SIZE_UNIT = TypedValue.COMPLEX_UNIT_PT;
-    private static final int DEFAULT_FONT_INDEX = 2;
-
-    private final Context mContext;
-    /** Container for mPreviewFontView to allow for scrollable padding at the top of the view. */
-    private ScrollView mScrollingContainer;
-    private TextView mPreviewFontView;
-    private Button mIncreaseFontButton;
-    private Button mDecreaseFontButton;
-
-    private final String[] mFontTwipValues;
-    private final String[] mFontSizeNames; // Ex: "Small".
-    /** Index into the above arrays for the saved preference value (from Gecko). */
-    private int mSavedFontIndex = DEFAULT_FONT_INDEX;
-    /** Index into the above arrays for the currently displayed font size (the preview). */
-    private int mPreviewFontIndex = mSavedFontIndex;
-    private final HashMap<String, Integer> mFontTwipToIndexMap;
-
-    public FontSizePreference(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        mContext = context;
-
-        final Resources res = mContext.getResources();
-        mFontTwipValues = res.getStringArray(R.array.pref_font_size_values);
-        mFontSizeNames = res.getStringArray(R.array.pref_font_size_entries);
-        mFontTwipToIndexMap = new HashMap<String, Integer>();
-        for (int i = 0; i < mFontTwipValues.length; ++i) {
-            mFontTwipToIndexMap.put(mFontTwipValues[i], i);
-        }
-    }
-
-    @Override
-    protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
-        final LayoutInflater inflater =
-            (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        View dialogView = inflater.inflate(R.layout.font_size_preference, null);
-        initInternalViews(dialogView);
-        updatePreviewFontSize(mFontTwipValues[mPreviewFontIndex]);
-
-        builder.setTitle(null);
-        builder.setView(dialogView);
-    }
-
-    /** Saves relevant views to instance variables and initializes their settings. */
-    private void initInternalViews(View dialogView) {
-        mScrollingContainer = (ScrollView) dialogView.findViewById(R.id.scrolling_container);
-        // Background cannot be set in XML (see bug 783597 - TODO: Change this to XML when bug is fixed).
-        mScrollingContainer.setBackgroundColor(Color.WHITE);
-        mPreviewFontView = (TextView) dialogView.findViewById(R.id.preview);
-
-        mDecreaseFontButton = (Button) dialogView.findViewById(R.id.decrease_preview_font_button);
-        mIncreaseFontButton = (Button) dialogView.findViewById(R.id.increase_preview_font_button);
-        setButtonState(mPreviewFontIndex);
-        mDecreaseFontButton.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                mPreviewFontIndex = Math.max(mPreviewFontIndex - 1, 0);
-                updatePreviewFontSize(mFontTwipValues[mPreviewFontIndex]);
-                mIncreaseFontButton.setEnabled(true);
-                // If we reached the minimum index, disable the button.
-                if (mPreviewFontIndex == 0) {
-                    mDecreaseFontButton.setEnabled(false);
-                }
-            }
-        });
-        mIncreaseFontButton.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                mPreviewFontIndex = Math.min(mPreviewFontIndex + 1, mFontTwipValues.length - 1);
-                updatePreviewFontSize(mFontTwipValues[mPreviewFontIndex]);
-
-                mDecreaseFontButton.setEnabled(true);
-                // If we reached the maximum index, disable the button.
-                if (mPreviewFontIndex == mFontTwipValues.length - 1) {
-                    mIncreaseFontButton.setEnabled(false);
-                }
-            }
-        });
-    }
-
-    @Override
-    protected void onDialogClosed(boolean positiveResult) {
-        super.onDialogClosed(positiveResult);
-        if (!positiveResult) {
-            mPreviewFontIndex = mSavedFontIndex;
-            return;
-        }
-        mSavedFontIndex = mPreviewFontIndex;
-        final String twipVal = mFontTwipValues[mSavedFontIndex];
-        final OnPreferenceChangeListener prefChangeListener = getOnPreferenceChangeListener();
-        if (prefChangeListener == null) {
-            Log.e(LOGTAG, "PreferenceChangeListener is null. FontSizePreference will not be saved to Gecko.");
-            return;
-        }
-        prefChangeListener.onPreferenceChange(this, twipVal);
-    }
-
-    /**
-     * Finds the index of the given twip value and sets it as the saved preference value. Also the
-     * current preview text size to the given value. Does not update the mPreviewFontView text size.
-     */
-    protected void setSavedFontSize(String twip) {
-        final Integer index = mFontTwipToIndexMap.get(twip);
-        if (index != null) {
-            mSavedFontIndex = index;
-            mPreviewFontIndex = mSavedFontIndex;
-            return;
-        }
-        resetSavedFontSizeToDefault();
-        Log.e(LOGTAG, "setSavedFontSize: Given font size does not exist in twip values map. Reverted to default font size.");
-    }
-
-    /**
-     * Updates the mPreviewFontView to the given text size, resets the container's scroll to the top
-     * left, and invalidates the view. Does not update the font indices.
-     */
-    private void updatePreviewFontSize(String twip) {
-        float pt = convertTwipStrToPT(twip);
-        // Android will not render a font size of 0 pt but for Gecko, 0 twip turns off font
-        // inflation. Thus we special case 0 twip to display a renderable font size.
-        if (pt == 0) {
-            // Android adds an inexplicable extra margin on the smallest font size so to get around
-            // this, we reinflate the view.
-            ViewGroup parentView = (ViewGroup) mScrollingContainer.getParent();
-            parentView.removeAllViews();
-            final LayoutInflater inflater =
-                (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-            View dialogView = inflater.inflate(R.layout.font_size_preference, parentView);
-            initInternalViews(dialogView);
-            mPreviewFontView.setTextSize(PREVIEW_FONT_SIZE_UNIT, 1);
-        } else {
-            mPreviewFontView.setTextSize(PREVIEW_FONT_SIZE_UNIT, pt);
-        }
-        mScrollingContainer.scrollTo(0, 0);
-    }
-
-    /**
-     * Resets the font indices to the default value. Does not update the mPreviewFontView text size.
-     */
-    private void resetSavedFontSizeToDefault() {
-        mSavedFontIndex = DEFAULT_FONT_INDEX;
-        mPreviewFontIndex = mSavedFontIndex;
-    }
-
-    private void setButtonState(int index) {
-        if (index == 0) {
-            mDecreaseFontButton.setEnabled(false);
-        } else if (index == mFontTwipValues.length - 1) {
-            mIncreaseFontButton.setEnabled(false);
-        }
-    }
-
-    /**
-     * Returns the name of the font size (ex: "Small") at the currently saved preference value.
-     */
-    protected String getSavedFontSizeName() {
-        return mFontSizeNames[mSavedFontIndex];
-    }
-
-    private float convertTwipStrToPT(String twip) {
-        return Float.parseFloat(twip) / TWIP_TO_PT_RATIO;
-    }
-}
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -169,16 +169,17 @@ public class GeckoPreferences
     public static final String PREFS_READ_PARTNER_BOOKMARKS_PROVIDER = NON_PREF_PREFIX + "distribution.read_partner_bookmarks_provider";
     public static final String PREFS_CUSTOM_TABS = NON_PREF_PREFIX + "customtabs";
     public static final String PREFS_ACTIVITY_STREAM = NON_PREF_PREFIX + "activitystream";
     public static final String PREFS_CATEGORY_EXPERIMENTAL_FEATURES = NON_PREF_PREFIX + "category_experimental";
     public static final String PREFS_COMPACT_TABS = NON_PREF_PREFIX + "compact_tabs";
     public static final String PREFS_SHOW_QUIT_MENU = NON_PREF_PREFIX + "distribution.show_quit_menu";
     public static final String PREFS_SEARCH_SUGGESTIONS_ENABLED = "browser.search.suggest.enabled";
     public static final String PREFS_DEFAULT_BROWSER = NON_PREF_PREFIX + "default_browser.link";
+    public static final String PREFS_SYSTEM_FONT_SIZE = NON_PREF_PREFIX + "font.size.use_system_font_size";
 
     private static final String ACTION_STUMBLER_UPLOAD_PREF = "STUMBLER_PREF";
 
 
     // This isn't a Gecko pref, even if it looks like one.
     private static final String PREFS_BROWSER_LOCALE = "locale";
 
     public static final String PREFS_RESTORE_SESSION = NON_PREF_PREFIX + "restoreSession3";
@@ -1265,19 +1266,16 @@ public class GeckoPreferences
         if (preference instanceof ListPreference) {
             // We need to find the entry for the new value
             int newIndex = ((ListPreference) preference).findIndexOfValue((String) newValue);
             CharSequence newEntry = ((ListPreference) preference).getEntries()[newIndex];
             ((ListPreference) preference).setSummary(newEntry);
         } else if (preference instanceof LinkPreference) {
             setResult(RESULT_CODE_EXIT_SETTINGS);
             finishChoosingTransition();
-        } else if (preference instanceof FontSizePreference) {
-            final FontSizePreference fontSizePref = (FontSizePreference) preference;
-            fontSizePref.setSummary(fontSizePref.getSavedFontSizeName());
         }
 
         return true;
     }
 
     private void enableStumbler(final CheckBoxPreference preference) {
         Permissions
                 .from(this)
@@ -1493,26 +1491,16 @@ public class GeckoPreferences
                     @Override
                     public void run() {
                         ((ListPreference) pref).setValue(value);
                         // Set the summary string to the current entry
                         CharSequence selectedEntry = ((ListPreference) pref).getEntry();
                         ((ListPreference) pref).setSummary(selectedEntry);
                     }
                 });
-            } else if (pref instanceof FontSizePreference) {
-                final FontSizePreference fontSizePref = (FontSizePreference) pref;
-                fontSizePref.setSavedFontSize(value);
-                final String fontSizeName = fontSizePref.getSavedFontSizeName();
-                ThreadUtils.postToUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        fontSizePref.setSummary(fontSizeName); // Ex: "Small".
-                    }
-                });
             }
         }
 
         @Override
         public void prefValue(String prefName, final int value) {
             final Preference pref = getField(prefName);
             Log.w(LOGTAG, "Unhandled int value for pref [" + pref + "]");
         }
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -349,36 +349,24 @@
 <!ENTITY pref_clear_private_data_now_tablet "Clear now">
 <!ENTITY pref_clear_on_exit_title3 "Clear private data on exit">
 <!ENTITY pref_clear_on_exit_summary2 "&brandShortName; will automatically clear your data whenever you select \u0022Quit\u0022 from the main menu">
 <!ENTITY pref_clear_on_exit_dialog_title "Select which data to clear">
 <!ENTITY pref_plugins "Plugins">
 <!ENTITY pref_plugins_enabled "Enabled">
 <!ENTITY pref_plugins_tap_to_play2 "Touch to play">
 <!ENTITY pref_plugins_disabled "Disabled">
-<!ENTITY pref_text_size "Text size">
 <!ENTITY pref_restore_tabs "Restore tabs">
 <!ENTITY pref_restore_always "Always restore">
 <!ENTITY pref_restore_quit "Don\'t restore after quitting &brandShortName;">
-<!ENTITY pref_font_size_tiny "Tiny">
-<!ENTITY pref_font_size_small "Small">
-<!ENTITY pref_font_size_medium "Medium">
-<!ENTITY pref_font_size_large "Large">
-<!ENTITY pref_font_size_xlarge "Extra Large">
-<!ENTITY pref_font_size_set "Set">
-<!-- Localization note (pref_font_size_adjust_char): A button with a small version of this character
-(or combination of characters) is used to decrease the preview font size; a larger version of the
-same character/combination is used to increase the preview font size. It should be a concise
-representation of the language it is used in that will help show the text in the preview will change
-size. -->
-<!ENTITY pref_font_size_adjust_char "A">
 
-<!-- Localization note (pref_font_size_preview_text): This paragraph is used as an example to
-    demonstrate the font size setting.  It is meant to be whimsical and fun. -->
-<!ENTITY pref_font_size_preview_text "The quick orange fox jumps over your expectations with more speed, more flexibility and more security. As a non-profit, we\'re free to innovate on your behalf without any pressure to compromise. That means a better experience for you and a brighter future for the Web.">
+<!-- Localization note (pref_use_system_font_size, pref_use_system_font_size_summary):
+     Font size here refers to the name of the corresponding Android system setting. -->
+<!ENTITY pref_use_system_font_size "Use system font size">
+<!ENTITY pref_use_system_font_size_summary "Scale web content according to the system font size">
 
 <!ENTITY pref_media_autoplay_enabled "Allow autoplay">
 <!ENTITY pref_media_autoplay_enabled_summary "Control if websites can autoplay videos and other media content">
 <!ENTITY pref_zoom_force_enabled "Always enable zoom">
 <!ENTITY pref_zoom_force_enabled_summary "Force override so you can zoom any page">
 <!ENTITY pref_voice_input "Voice input">
 <!ENTITY pref_voice_input_summary2 "Allow voice dictation in the URL bar">
 <!ENTITY pref_qrcode_enabled "QR code reader">
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -448,16 +448,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'firstrun/RestrictedWelcomePanel.java',
     'firstrun/SyncPanel.java',
     'firstrun/TabQueuePanel.java',
     'FormAssistPopup.java',
     'GeckoActivity.java',
     'GeckoActivityStatus.java',
     'GeckoApp.java',
     'GeckoApplication.java',
+    'GeckoFontScaleListener.java',
     'GeckoJavaSampler.java',
     'GeckoMessageReceiver.java',
     'GeckoProfilesProvider.java',
     'GeckoService.java',
     'GeckoUpdateReceiver.java',
     'GlobalHistory.java',
     'GlobalPageMetadata.java',
     'GuestSession.java',
@@ -647,17 +648,16 @@ gbjar.sources += ['java/org/mozilla/geck
     'preferences/AndroidImport.java',
     'preferences/AndroidImportPreference.java',
     'preferences/AppCompatPreferenceActivity.java',
     'preferences/ClearOnShutdownPref.java',
     'preferences/CustomCheckBoxPreference.java',
     'preferences/CustomListCategory.java',
     'preferences/CustomListPreference.java',
     'preferences/DistroSharedPrefsImport.java',
-    'preferences/FontSizePreference.java',
     'preferences/GeckoPreferenceFragment.java',
     'preferences/GeckoPreferences.java',
     'preferences/LinkPreference.java',
     'preferences/ListCheckboxPreference.java',
     'preferences/LocaleListPreference.java',
     'preferences/ModifiableHintPreference.java',
     'preferences/MultiChoicePreference.java',
     'preferences/MultiPrefMultiChoicePreference.java',
deleted file mode 100644
--- a/mobile/android/base/resources/layout-xlarge-v11/font_size_preference.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 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/.  -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:orientation="vertical"> 
-
-    <ScrollView android:id="@+id/scrolling_container"
-                android:layout_width="match_parent"
-                android:layout_height="350dp"
-                android:layout_margin="8dp"
-                android:padding="8dp"
-                android:scrollbars="vertical"
-                android:scrollbarStyle="outsideOverlay"
-                android:fadeScrollbars="true"
-                android:requiresFadingEdge="vertical">
-
-            <TextView android:id="@+id/preview"
-                      android:layout_width="match_parent"
-                      android:layout_height="wrap_content"
-                      android:text="@string/pref_font_size_preview_text"
-                      android:textColor="#ff000000"/>
-
-    </ScrollView>
-
-    <LinearLayout android:id="@+id/button_container"
-                  android:layout_width="match_parent"
-                  android:layout_height="wrap_content"
-                  android:layout_marginLeft="4dp"
-                  android:layout_marginRight="4dp"
-                  android:orientation="horizontal">
-
-        <Button android:id="@+id/decrease_preview_font_button"
-                android:layout_width="0dp"
-                android:layout_height="match_parent"
-                android:layout_weight="1"
-                android:text="@string/pref_font_size_adjust_char"
-                android:textSize="8sp"/>
-
-        <Button android:id="@+id/increase_preview_font_button"
-                android:layout_width="0dp"
-                android:layout_height="match_parent"
-                android:layout_weight="1"
-                android:text="@string/pref_font_size_adjust_char"
-                android:textSize="16sp"/>
-
-    </LinearLayout>
-
-</LinearLayout>
deleted file mode 100644
--- a/mobile/android/base/resources/layout/font_size_preference.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 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/.  -->
-
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent">
-
-    <LinearLayout android:id="@+id/button_container"
-                  android:layout_width="match_parent"
-                  android:layout_height="wrap_content"
-                  android:layout_marginLeft="4dp"
-                  android:layout_marginRight="4dp"
-                  android:layout_alignParentBottom="true"
-                  android:layout_alignParentLeft="true"
-                  android:orientation="horizontal">
-
-        <Button android:id="@+id/decrease_preview_font_button"
-                android:layout_width="0dp"
-                android:layout_height="match_parent"
-                android:layout_weight="1"
-                android:text="@string/pref_font_size_adjust_char"
-                android:textSize="8sp"/>
-
-        <Button android:id="@+id/increase_preview_font_button"
-                android:layout_width="0dp"
-                android:layout_height="match_parent"
-                android:layout_weight="1"
-                android:text="@string/pref_font_size_adjust_char"
-                android:textSize="16sp"/>
-
-    </LinearLayout>
-
-    <ScrollView android:id="@+id/scrolling_container"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:layout_above="@id/button_container"
-                android:layout_margin="8dp"
-                android:padding="8dp"
-                android:scrollbars="vertical"
-                android:scrollbarStyle="outsideOverlay"
-                android:fadeScrollbars="true"
-                android:requiresFadingEdge="vertical">
-
-            <TextView android:id="@+id/preview"
-                      android:layout_width="match_parent"
-                      android:layout_height="wrap_content"
-                      android:text="@string/pref_font_size_preview_text"
-                      android:textColor="#ff000000"/>
-
-    </ScrollView>
-
-</RelativeLayout>
--- a/mobile/android/base/resources/values/arrays.xml
+++ b/mobile/android/base/resources/values/arrays.xml
@@ -18,30 +18,16 @@
         <item>@string/pref_plugins_tap_to_play</item>
         <item>@string/pref_plugins_disabled</item>
     </string-array>
     <string-array name="pref_plugins_values">
         <item>1</item>
         <item>2</item>
         <item>0</item>
     </string-array>
-    <string-array name="pref_font_size_entries">
-        <item>@string/pref_font_size_tiny</item>
-        <item>@string/pref_font_size_small</item>
-        <item>@string/pref_font_size_medium</item>
-        <item>@string/pref_font_size_large</item>
-        <item>@string/pref_font_size_xlarge</item>
-    </string-array>
-    <string-array name="pref_font_size_values">
-        <item>0</item>
-        <item>80</item>
-        <item>120</item>
-        <item>160</item>
-        <item>240</item>
-    </string-array>
     <string-array name="pref_char_encoding_entries">
         <item>@string/pref_char_encoding_on</item>
         <item>@string/pref_char_encoding_off</item>
     </string-array>
     <string-array name="pref_char_encoding_values">
         <item>true</item>
         <item>false</item>
     </string-array>
--- a/mobile/android/base/resources/xml/preferences_accessibility.xml
+++ b/mobile/android/base/resources/xml/preferences_accessibility.xml
@@ -2,22 +2,19 @@
 <!-- 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/. -->
 
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
                   android:title="@string/pref_category_accessibility"
                   android:enabled="false">
 
-    <org.mozilla.gecko.preferences.FontSizePreference
-                    android:key="font.size.inflation.minTwips"
-                    android:title="@string/pref_text_size"
-                    android:positiveButtonText="@string/pref_font_size_set"
-                    android:negativeButtonText="@string/button_cancel"
-                    android:persistent="false" />
+    <SwitchPreference android:key="android.not_a_preference.font.size.use_system_font_size"
+                      android:title="@string/pref_use_system_font_size"
+                      android:summary="@string/pref_use_system_font_size_summary" />
 
     <SwitchPreference android:key="browser.ui.zoom.force-user-scalable"
                       android:title="@string/pref_zoom_force_enabled"
                       android:summary="@string/pref_zoom_force_enabled_summary" />
 
     <SwitchPreference android:key="ui.zoomedview.enabled"
                       android:title="@string/pref_magnifying_glass_enabled"
                       android:summary="@string/pref_magnifying_glass_enabled_summary" />
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -252,25 +252,18 @@
   <string name="pref_clear_private_data_now_tablet">&pref_clear_private_data_now_tablet;</string>
   <string name="pref_clear_on_exit_title">&pref_clear_on_exit_title3;</string>
   <string name="pref_clear_on_exit_summary2">&pref_clear_on_exit_summary2;</string>
   <string name="pref_clear_on_exit_dialog_title">&pref_clear_on_exit_dialog_title;</string>
   <string name="pref_plugins">&pref_plugins;</string>
   <string name="pref_plugins_enabled">&pref_plugins_enabled;</string>
   <string name="pref_plugins_tap_to_play">&pref_plugins_tap_to_play2;</string>
   <string name="pref_plugins_disabled">&pref_plugins_disabled;</string>
-  <string name="pref_text_size">&pref_text_size;</string>
-  <string name="pref_font_size_tiny">&pref_font_size_tiny;</string>
-  <string name="pref_font_size_small">&pref_font_size_small;</string>
-  <string name="pref_font_size_medium">&pref_font_size_medium;</string>
-  <string name="pref_font_size_large">&pref_font_size_large;</string>
-  <string name="pref_font_size_xlarge">&pref_font_size_xlarge;</string>
-  <string name="pref_font_size_set">&pref_font_size_set;</string>
-  <string name="pref_font_size_adjust_char">&pref_font_size_adjust_char;</string>
-  <string name="pref_font_size_preview_text">&pref_font_size_preview_text;</string>
+  <string name="pref_use_system_font_size">&pref_use_system_font_size;</string>
+  <string name="pref_use_system_font_size_summary">&pref_use_system_font_size_summary;</string>
   <string name="pref_media_autoplay_enabled">&pref_media_autoplay_enabled;</string>
   <string name="pref_media_autoplay_enabled_summary">&pref_media_autoplay_enabled_summary;</string>
   <string name="pref_zoom_force_enabled">&pref_zoom_force_enabled;</string>
   <string name="pref_zoom_force_enabled_summary">&pref_zoom_force_enabled_summary;</string>
   <string name="pref_voice_input">&pref_voice_input;</string>
   <string name="pref_voice_input_summary">&pref_voice_input_summary2;</string>
   <string name="pref_qrcode_enabled">&pref_qrcode_enabled;</string>
   <string name="pref_qrcode_enabled_summary">&pref_qrcode_enabled_summary2;</string>
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/PrefsHelper.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/PrefsHelper.java
@@ -26,17 +26,16 @@ public final class PrefsHelper {
     // Map pref name to ArrayList for multiple observers or PrefHandler for single observer.
     private static final SimpleArrayMap<String, Object> OBSERVERS = new SimpleArrayMap<>();
     private static final HashSet<String> INT_TO_STRING_PREFS = new HashSet<>(8);
     private static final HashSet<String> INT_TO_BOOL_PREFS = new HashSet<>(2);
 
     static {
         INT_TO_STRING_PREFS.add("browser.chrome.titlebarMode");
         INT_TO_STRING_PREFS.add("network.cookie.cookieBehavior");
-        INT_TO_STRING_PREFS.add("font.size.inflation.minTwips");
         INT_TO_STRING_PREFS.add("home.sync.updateMode");
         INT_TO_STRING_PREFS.add("browser.image_blocking");
         INT_TO_BOOL_PREFS.add("browser.display.use_document_fonts");
     }
 
     @WrapForJNI
     private static final int PREF_INVALID = -1;
     @WrapForJNI
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/basic_article_mobile_2x.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Article title</title>
+<meta name="description" content="This is the article description." />
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+</head>
+<style>
+body { font-size: 200% !important; }
+</style>
+<body>
+<header>Site header</header>
+<div>
+<h1>Article title</h1>
+<h2 class="author">by Jane Doe</h2>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+</div>
+</body>
+</html>
--- a/mobile/android/tests/browser/chrome/chrome.ini
+++ b/mobile/android/tests/browser/chrome/chrome.ini
@@ -1,13 +1,14 @@
 [DEFAULT]
 skip-if = os != 'android'
 support-files =
   basic_article.html
   basic_article_mobile.html
+  basic_article_mobile_2x.html
   desktopmode_user_agent.sjs
   devicesearch.xml
   head.js
   head_search.js
   session_formdata_sample.html
   simpleservice.xml
   video_controls.html
   video_discovery.html
@@ -40,12 +41,13 @@ skip-if = true # Bug 1241478
 [test_restricted_profiles.html]
 [test_select_disabled.html]
 [test_selectoraddtab.html]
 [test_session_clear_history.html]
 [test_session_form_data.html]
 [test_session_parentid.html]
 [test_session_scroll_position.html]
 [test_session_zombification.html]
+[test_settings_fontinflation.html]
 [test_shared_preferences.html]
 [test_simple_discovery.html]
 [test_video_discovery.html]
 [test_web_channel.html]
--- a/mobile/android/tests/browser/chrome/head.js
+++ b/mobile/android/tests/browser/chrome/head.js
@@ -65,8 +65,28 @@ function promiseLinkVisit(url) {
       info("Visited URL " + uri.spec + " is desired URL " + url);
       Services.obs.removeObserver(observe, topic);
       resolve();
     };
     Services.obs.addObserver(observe, topic, false);
     info("Now waiting for " + topic + " notification from Gecko with URL " + url);
   });
 }
+
+function makeObserver(observerId) {
+  let deferred = Promise.defer();
+
+  let ret = {
+    id: observerId,
+    count: 0,
+    promise: deferred.promise,
+    observe: function (subject, topic, data) {
+      ret.count += 1;
+      let msg = { subject: subject,
+                  topic: topic,
+                  data: data };
+      deferred.resolve(msg);
+    },
+  };
+
+  return ret;
+};
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_settings_fontinflation.html
@@ -0,0 +1,230 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1328868
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1328868</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script>
+  <script type="application/javascript" src="head.js"></script>
+  <script type="application/javascript">
+
+  /** Test for Bug 1328868 **/
+
+  "use strict";
+
+  const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+  Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+  Cu.import("resource://gre/modules/Services.jsm");
+  Cu.import("resource://gre/modules/Messaging.jsm");
+  Cu.import("resource://gre/modules/Promise.jsm");
+  Cu.import("resource://gre/modules/Task.jsm");
+  Cu.import("resource://gre/modules/SharedPreferences.jsm");
+
+  const GECKO_PREF_FONT_INFLATION = "font.size.inflation.minTwips";
+  const FONT_INFLATION_DEFAULT_VALUE = 120;
+  const GECKO_PREF_FONT_SCALE = "font.size.systemFontScale";
+  const ANDROID_PREF = "android.not_a_preference.font.size.use_system_font_size";
+
+  // The chrome window
+  let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+  let BrowserApp = chromeWin.BrowserApp;
+
+  // Track the tabs where the tests are happening
+  let tab;
+
+  // This page will be eligible for font inflation...
+  const URL_desktop = "http://example.org/chrome/mobile/android/tests/browser/chrome/basic_article.html";
+  // ... and this one won't.
+  const URL_mobile = "http://example.org/chrome/mobile/android/tests/browser/chrome/basic_article_mobile.html";
+  const URL_mobile_2x = "http://example.org/chrome/mobile/android/tests/browser/chrome/basic_article_mobile_2x.html";
+
+  let sharedPrefs = SharedPreferences.forApp();
+  let _observerId = 0;
+
+  function cleanupTabs() {
+    if (tab) {
+      BrowserApp.closeTab(tab);
+      tab = null;
+    }
+  }
+
+  function setSystemFontScale(scale) {
+    Services.prefs.setIntPref(GECKO_PREF_FONT_SCALE, Math.round(scale * 100));
+  }
+
+  function getSystemFontScale() {
+    return Services.prefs.getIntPref(GECKO_PREF_FONT_SCALE) / 100;
+  }
+
+  function resetPrefs() {
+    sharedPrefs.setBoolPref(ANDROID_PREF, false);
+    Services.prefs.setIntPref(GECKO_PREF_FONT_INFLATION, 0);
+    setSystemFontScale(1.0);
+  }
+
+  SimpleTest.registerCleanupFunction(function() {
+    // If anything goes wrong, we want to be sure to leave everything as we came
+    resetPrefs();
+    cleanupTabs();
+  });
+
+  add_task(function* test_sysFontScaleScalesMobilePages() {
+    // Check that we're starting out with the default values
+    is(sharedPrefs.getBoolPref(ANDROID_PREF), false, "System font size scaling is disabled");
+    is(Services.prefs.getIntPref(GECKO_PREF_FONT_INFLATION), 0, "Gecko-side font inflation is disabled");
+
+    // Check the system font scale factor and then enable it for testing
+    is(getSystemFontScale(), 1.0, "system font scale is default");
+    tab = BrowserApp.addTab(URL_mobile , { selected: true, parentId: BrowserApp.selectedTab.id });
+    yield promiseBrowserEvent(tab.browser, "load");
+    is(tab.browser.effectiveTextZoom, 1.0, "text zoom is default value");
+
+    setSystemFontScale(2.0);
+    tab.reloadWithMode(tab.desktopMode);
+    yield promiseBrowserEvent(tab.browser, "load");
+
+    is(getSystemFontScale(), 2.0, "system font scale is enabled");
+    is(tab.browser.effectiveTextZoom, 2.0, "text zoom set through system font scale");
+    let fontScaleOn = snapshotWindow(tab.browser.contentWindow);
+
+    // Reset the system font scale again
+    setSystemFontScale(1.0);
+    is(getSystemFontScale(), 1.0, "system font scale is default");
+
+    tab.reloadWithMode(tab.desktopMode);
+    yield promiseBrowserEvent(tab.browser, "load");
+
+    is(tab.browser.effectiveTextZoom, 1.0, "text zoom is back to default value");
+    let fontScaleOff = snapshotWindow(tab.browser.contentWindow);
+    assertSnapshots(fontScaleOn, fontScaleOff, false, null, "fontScaleOn", "fontScaleOff");
+
+    // Now compare with a document that's been zoomed through CSS
+    tab.browser.loadURI(URL_mobile_2x);
+    yield promiseBrowserEvent(tab.browser, "load");
+
+    let cssZoom = snapshotWindow(tab.browser.contentWindow);
+    assertSnapshots(fontScaleOn, cssZoom, true, null, "fontScaleOn", "cssZoom");
+
+    // Load the original document and test normal text zooming
+    tab.browser.loadURI(URL_mobile);
+    yield promiseBrowserEvent(tab.browser, "load");
+
+    tab.browser.textZoom = 2.0;
+    is(tab.browser.effectiveTextZoom, 2.0, "text zoom is enabled");
+
+    let textZoom = snapshotWindow(tab.browser.contentWindow);
+    assertSnapshots(fontScaleOn, textZoom, true, null, "fontScaleOn", "textZoom");
+
+    cleanupTabs();
+  });
+
+  add_task(function* test_fontInflationPrecedence() {
+    // Check that we're starting out with the default values
+    is(sharedPrefs.getBoolPref(ANDROID_PREF), false, "System font size scaling is disabled");
+    is(Services.prefs.getIntPref(GECKO_PREF_FONT_INFLATION), 0, "Gecko-side font inflation is disabled");
+
+    // Check the system font scale factor and then take a screenshot of this base state
+    is(getSystemFontScale(), 1.0, "system font scale is default");
+    tab = BrowserApp.addTab(URL_desktop , { selected: true, parentId: BrowserApp.selectedTab.id });
+    yield promiseBrowserEvent(tab.browser, "load");
+    is(tab.browser.effectiveTextZoom, 1.0, "text zoom is default value");
+
+    let noZoom = snapshotWindow(tab.browser.contentWindow);
+
+    // Enable font inflation and check that this has some effect
+    Services.prefs.setIntPref(GECKO_PREF_FONT_INFLATION, FONT_INFLATION_DEFAULT_VALUE);
+
+    tab.reloadWithMode(tab.desktopMode);
+    yield promiseBrowserEvent(tab.browser, "load");
+
+    let fontInflationOn = snapshotWindow(tab.browser.contentWindow);
+    assertSnapshots(noZoom, fontInflationOn, false, null, "noZoom", "fontInflationOn");
+
+    // Now enable the system font scale and make sure that this doesn't result in any visual change
+    setSystemFontScale(2.0);
+    tab.reloadWithMode(tab.desktopMode);
+    yield promiseBrowserEvent(tab.browser, "load");
+
+    is(getSystemFontScale(), 2.0, "system font scale is enabled");
+    is(tab.browser.effectiveTextZoom, 1.0, "text zoom remains at default value");
+    let fontScaleWithFontInflation = snapshotWindow(tab.browser.contentWindow);
+    assertSnapshots(fontInflationOn, fontScaleWithFontInflation, true, null, "fontInflationOn", "fontScaleWithFontInflation");
+
+    // Disable font inflation and check that system font scale zooming becomes active
+    Services.prefs.setIntPref(GECKO_PREF_FONT_INFLATION, 0);
+    tab.reloadWithMode(tab.desktopMode);
+    yield promiseBrowserEvent(tab.browser, "load");
+
+    is(tab.browser.effectiveTextZoom, 2.0, "text zoom set through system font scale zooming");
+    let fontScaleNoFontInflation = snapshotWindow(tab.browser.contentWindow);
+    assertSnapshots(noZoom, fontScaleNoFontInflation, false, null, "noZoom", "fontScaleNoFontInflation");
+
+    // Reset system font scale back to default
+    setSystemFontScale(1.0);
+
+    cleanupTabs();
+  });
+
+  // There is a slight delay between flipping the Android shared pref and the
+  // Java side listener native call actually getting through to the font scale in
+  // nsLayoutUtils, therefore this test MUST run last, otherwise the font scale
+  // could unexpectedly be reset in the middle of a following test.
+  // However as as long the test environment uses the default Android system font scale,
+  // this won't impact other unrelated tests since we set "font.size.systemFontScale"
+  // to 1.0 in that case, which already is the default value.
+  add_task(function* test_androidPrefControlsFontInflation() {
+    // Check that we're starting out with the default values
+    is(sharedPrefs.getBoolPref(ANDROID_PREF), false, "System font size scaling is disabled");
+    is(Services.prefs.getIntPref(GECKO_PREF_FONT_INFLATION), 0, "Gecko-side font inflation is disabled");
+    is(getSystemFontScale(), 1.0, "system font scale is default");
+
+    // Flipping the Android pref "on" should enable font inflation
+    let observer = makeObserver(_observerId++);
+    Services.prefs.addObserver(GECKO_PREF_FONT_INFLATION, observer, false);
+
+    try {
+      sharedPrefs.setBoolPref(ANDROID_PREF, true);
+      let result = yield observer.promise;
+
+      is(observer.count, 1, "Gecko pref should have changed only once");
+      is(result.data, GECKO_PREF_FONT_INFLATION, "the correct pref has changed");
+      is(Services.prefs.getIntPref(GECKO_PREF_FONT_INFLATION), FONT_INFLATION_DEFAULT_VALUE, "Gecko-side font inflation is enabled");
+    } finally {
+      Services.prefs.removeObserver(GECKO_PREF_FONT_INFLATION, observer);
+    }
+
+    // ... and turning it back off should disable it again.
+    observer = makeObserver(_observerId++);
+    Services.prefs.addObserver(GECKO_PREF_FONT_INFLATION, observer, false);
+
+    try {
+      sharedPrefs.setBoolPref(ANDROID_PREF, false);
+      let result = yield observer.promise;
+
+      is(observer.count, 1, "Gecko pref should have changed only once");
+      is(result.data, GECKO_PREF_FONT_INFLATION, "the correct pref has changed");
+      is(Services.prefs.getIntPref(GECKO_PREF_FONT_INFLATION), 0, "Gecko-side font inflation is disabled");
+    } finally {
+      Services.prefs.removeObserver(GECKO_PREF_FONT_INFLATION, observer);
+    }
+  });
+
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1328868">Mozilla Bug 1328868</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
--- a/mobile/android/tests/browser/chrome/test_shared_preferences.html
+++ b/mobile/android/tests/browser/chrome/test_shared_preferences.html
@@ -6,43 +6,25 @@ Migrated from Robocop: https://bugzilla.
 -->
 <head>
   <meta charset="utf-8">
   <title>Test for Bug 866271</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript">
 
   Components.utils.import("resource://gre/modules/SharedPreferences.jsm");
   Components.utils.import("resource://gre/modules/Promise.jsm");
   Components.utils.import("resource://gre/modules/Task.jsm");
 
   let _observerId = 0;
 
-  function makeObserver() {
-    let deferred = Promise.defer();
-
-    let ret = {
-      id: _observerId++,
-      count: 0,
-      promise: deferred.promise,
-      observe: function (subject, topic, data) {
-        ret.count += 1;
-        let msg = { subject: subject,
-                    topic: topic,
-                    data: data };
-        deferred.resolve(msg);
-      },
-    };
-
-    return ret;
-  };
-
   add_task(function* test_get_set() {
     let branch = SharedPreferences.forAndroid("test");
 
     branch.setBoolPref("boolKey", true);
     branch.setCharPref("charKey", "string value");
     branch.setIntPref("intKey", 1000);
 
     is(branch.getBoolPref("boolKey"), true);
@@ -104,17 +86,17 @@ Migrated from Robocop: https://bugzilla.
   });
 
   add_task(function* test_add_remove_observer() {
     let branch = SharedPreferences.forAndroid("test");
 
     branch.setBoolPref("boolKey", false);
     is(branch.getBoolPref("boolKey"), false);
 
-    let obs1 = makeObserver();
+    let obs1 = makeObserver(_observerId++);
     branch.addObserver("boolKey", obs1);
 
     try {
       branch.setBoolPref("boolKey", true);
       is(branch.getBoolPref("boolKey"), true);
 
       let value1 = yield obs1.promise;
       is(obs1.count, 1);
@@ -128,17 +110,17 @@ Migrated from Robocop: https://bugzilla.
     }
 
     // Make sure the original observer is really gone, or as close as
     // we: install a second observer, wait for it to be notified, and
     // then verify the original observer was *not* notified.  This
     // depends, of course, on the order that observers are notified, but
     // is better than nothing.
 
-    let obs2 = makeObserver();
+    let obs2 = makeObserver(_observerId++);
     branch.addObserver("boolKey", obs2);
 
     try {
       branch.setBoolPref("boolKey", false);
       is(branch.getBoolPref("boolKey"), false);
 
       let value2 = yield obs2.promise;
       is(obs2.count, 1);
@@ -156,17 +138,17 @@ Migrated from Robocop: https://bugzilla.
   });
 
   add_task(function* test_observer_ignores() {
     let branch = SharedPreferences.forAndroid("test");
 
     branch.setCharPref("charKey", "first value");
     is(branch.getCharPref("charKey"), "first value");
 
-    let obs = makeObserver();
+    let obs = makeObserver(_observerId++);
     branch.addObserver("charKey", obs);
 
     try {
       // These should all be ignored.
       branch.setBoolPref("boolKey", true);
       branch.setBoolPref("boolKey", false);
       branch.setIntPref("intKey", -3000);
       branch.setIntPref("intKey", 4000);
@@ -187,17 +169,17 @@ Migrated from Robocop: https://bugzilla.
   });
 
   add_task(function* test_observer_ignores_branches() {
     let branch = SharedPreferences.forAndroid("test");
 
     branch.setCharPref("charKey", "first value");
     is(branch.getCharPref("charKey"), "first value");
 
-    let obs = makeObserver();
+    let obs = makeObserver(_observerId++);
     branch.addObserver("charKey", obs);
 
     try {
       // These should all be ignored.
       let branch2 = SharedPreferences.forAndroid("test2");
       branch2.setCharPref("charKey", "a wrong value");
       let branch3 = SharedPreferences.forAndroid("test.2");
       branch3.setCharPref("charKey", "a different wrong value");
--- a/mobile/android/themes/core/config.css
+++ b/mobile/android/themes/core/config.css
@@ -9,18 +9,19 @@ body {
     background-color: #ced7de;
     -moz-user-select: none;
     font-family: "Clear Sans",sans-serif;
     -moz-text-size-adjust: none;
 }
 
 .toolbar {
     width: 100%;
-    height: 3em;
-    position: fixed;
+    min-height: 3em;
+    display: flow-root;
+    position: sticky;
     top: 0;
     left: 0;
     z-index: 10;
     box-shadow: 0 0 3px #444;
     background-color: #ced7de;
     color: #000000;
     font-weight: bold;
     border-bottom: 2px solid;
@@ -29,17 +30,17 @@ body {
 
 .toolbar-container {
     max-width: 40em;
     margin-left: auto;
     margin-right: auto;
 }
 
 #filter-container {
-    margin: 0.5em;
+    margin: 0.375em;
     height: 2em;
     border: 1px solid transparent;
     border-image-source: url("chrome://browser/skin/images/textfield.png");
     border-image-slice: 1 1 3 1;
     border-image-width: 1px 1px 3px 1px;
     overflow: hidden;
     display: flex;
     flex-direction: row;
@@ -73,42 +74,42 @@ body {
     min-width: 3em;
     box-sizing: border-box;
     opacity: 0.75;
 }
 
 #new-pref-toggle-button {
     background-position: center center;
     background-image: url("chrome://browser/skin/images/config-plus.png");
-    background-size: 48px 48px;
-    height: 48px;
-    width: 48px;
+    background-size: 3em 3em;
+    height: 3em;
+    width: 3em;
     display: inline-block;
     outline-style: none;
     float: left;
 }
 
 #new-pref-toggle-button:dir(rtl) {
     float: right;
 }
 
 #filter-search-button {
     background-image: url("chrome://browser/skin/images/search.png");
-    background-size: 32px 32px;
-    height: 32px;
-    width: 32px;
+    background-size: 2em 2em;
+    height: 2em;
+    width: 2em;
     display: inline-block;
     outline-style: none;
 }
 
 #filter-input-clear-button {
     background-image: url("chrome://browser/skin/images/search-clear-30.png");
-    background-size: 32px 32px;
-    height: 32px;
-    width: 32px;
+    background-size: 2em 2em;
+    height: 2em;
+    width: 2em;
     display: inline-block;
     outline-style: none;
 }
 
 #filter-input[value=""] + #filter-input-clear-button {
     display: none;
 }
 
@@ -119,17 +120,16 @@ body {
     float: right;
 }
 
 #content {
     position: relative;
     margin: 0;
     margin-left: auto;
     margin-right: auto;
-    padding-top: 3em;
     padding-left: 0;
     padding-right: 0;
     min-height: 100%;
     max-width: 40em;
 }
 
 ul {
     list-style-position: inside;
@@ -267,18 +267,19 @@ li {
     color: #d3d3d3;
 }
 
 .pref-item.selected {
     background-color: rgba(0,0,255,0.05);
 }
 
 .pref-button {
-    display: inline-block;
+    display: inline-flex;
     box-sizing: border-box;
+    align-items: center;
     text-align: center;
     padding: 10px 1em;
     border-left: 1px solid rgba(0,0,0,0.1);
     opacity: 0;
     transition-property: opacity;
     transition-duration: 500ms;
 }
 
@@ -299,22 +300,24 @@ li {
 }
 
 .pref-button[disabled] {
     display: none;
 }
 
 .pref-button.up {
     background-image: url("chrome://browser/skin/images/arrowup-16.png");
+    background-size: 1em 1em;
     background-position: center center;
     background-repeat: no-repeat;
 }
 
 .pref-button.down {
     background-image: url("chrome://browser/skin/images/arrowdown-16.png");
+    background-size: 1em 1em;
     background-position: center center;
     background-repeat: no-repeat;
 }
 
 #prefs-shield {
     width: 100%;
     height: 100%;
     background-color: rgba(0,0,0,0.5);
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -3216,16 +3216,27 @@ pref("font.size.inflation.mappingInterce
  * i/s * 100 should never exceed the value of this preference.
  *
  * In order for this preference to have any effect, its value must be
  * greater than 100, since font inflation can never decrease the ratio
  * i/s.
  */
 pref("font.size.inflation.maxRatio", 0);
 
+/**
+ * This setting corresponds to a global text zoom setting affecting
+ * all content that is not already subject to font size inflation.
+ * It is interpreted as a percentage value that is applied on top
+ * of the document's current text zoom setting.
+ *
+ * The resulting total zoom factor (text zoom * system font scale)
+ * will be limited by zoom.minPercent and maxPercent.
+ */
+pref("font.size.systemFontScale", 100);
+
 /*
  * When enabled, the touch.radius and mouse.radius prefs allow events to be dispatched
  * to nearby elements that are sensitive to the event. See PositionedEventTargeting.cpp.
  * The 'mm' prefs define a rectangle around the nominal event target point within which
  * we will search for suitable elements. 'visitedWeight' is a percentage weight;
  * a value > 100 makes a visited link be treated as further away from the event target
  * than it really is, while a value < 100 makes a visited link be treated as closer
  * to the event target than it really is.
--- a/toolkit/content/widgets/browser.xml
+++ b/toolkit/content/widgets/browser.xml
@@ -547,16 +547,23 @@
         <getter><![CDATA[
           return this.markupDocumentViewer.textZoom;
         ]]></getter>
         <setter><![CDATA[
           this.markupDocumentViewer.textZoom = val;
         ]]></setter>
       </property>
 
+      <property name="effectiveTextZoom"
+                readonly="true">
+        <getter><![CDATA[
+          return this.markupDocumentViewer.effectiveTextZoom;
+        ]]></getter>
+      </property>
+
       <property name="isSyntheticDocument">
         <getter><![CDATA[
           return this.contentDocument.mozSyntheticDocument;
         ]]></getter>
       </property>
 
       <property name="hasContentOpener">
         <getter><![CDATA[