Bug 979166 - Refactor dynamic toolbar code. r=lucasr
authorBrian Nicholson <bnicholson@mozilla.com>
Thu, 13 Mar 2014 12:29:39 -0700
changeset 173474 76d856e4ec61d8702bb5bb06351319823d34707c
parent 173473 21793ea94b09b2c2df3a3cefac56ef7822e5d96f
child 173475 eaabf46d60ad68421bcc9258ad9e2e1939d65b06
push id5674
push userbnicholson@mozilla.com
push dateThu, 13 Mar 2014 20:38:31 +0000
treeherderfx-team@76d856e4ec61 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslucasr
bugs979166
milestone30.0a1
Bug 979166 - Refactor dynamic toolbar code. r=lucasr
mobile/android/base/BrowserApp.java
mobile/android/base/DynamicToolbar.java
mobile/android/base/moz.build
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -9,16 +9,18 @@ import java.io.File;
 import java.io.FileNotFoundException;
 import java.net.URLEncoder;
 import java.util.EnumSet;
 import java.util.Vector;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
+import org.mozilla.gecko.DynamicToolbar.PinReason;
+import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.LoadFaviconTask;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
@@ -103,29 +105,26 @@ abstract public class BrowserApp extends
                                             GeckoLayerClient.OnMetricsChangedListener,
                                             BrowserSearch.OnSearchListener,
                                             BrowserSearch.OnEditSuggestionListener,
                                             HomePager.OnNewTabsListener,
                                             OnUrlOpenListener,
                                             ActionModeCompat.Presenter {
     private static final String LOGTAG = "GeckoBrowserApp";
 
-    private static final String PREF_CHROME_DYNAMICTOOLBAR = "browser.chrome.dynamictoolbar";
-
     private static final int TABS_ANIMATION_DURATION = 450;
 
     private static final int READER_ADD_SUCCESS = 0;
     private static final int READER_ADD_FAILED = 1;
     private static final int READER_ADD_DUPLICATE = 2;
 
     private static final String ADD_SHORTCUT_TOAST = "add_shortcut_toast";
     public static final String GUEST_BROWSING_ARG = "--guest";
 
     private static final String STATE_ABOUT_HOME_TOP_PADDING = "abouthome_top_padding";
-    private static final String STATE_DYNAMIC_TOOLBAR_ENABLED = "dynamic_toolbar";
 
     private static final String BROWSER_SEARCH_TAG = "browser_search";
     private BrowserSearch mBrowserSearch;
     private View mBrowserSearchContainer;
 
     public ViewFlipper mViewFlipper;
     public ActionModeCompatView mActionBar;
     private BrowserToolbar mBrowserToolbar;
@@ -165,48 +164,43 @@ abstract public class BrowserApp extends
             t -= 1.0f;
             return t * t * t * t * t + 1.0f;
         }
     };
 
     private FindInPageBar mFindInPageBar;
     private MediaCastingBar mMediaCastingBar;
 
-    private boolean mAccessibilityEnabled = false;
-
     // We'll ask for feedback after the user launches the app this many times.
     private static final int FEEDBACK_LAUNCH_COUNT = 15;
 
-    // Whether the dynamic toolbar pref is enabled.
-    private boolean mDynamicToolbarEnabled = false;
-
     // Stored value of the toolbar height, so we know when it's changed.
     private int mToolbarHeight = 0;
 
     // Stored value of whether the last metrics change allowed for toolbar
     // scrolling.
     private boolean mDynamicToolbarCanScroll = false;
 
-    private Integer mPrefObserverId;
-
     private SharedPreferencesHelper mSharedPreferencesHelper;
 
     private OrderedBroadcastHelper mOrderedBroadcastHelper;
 
     private BrowserHealthReporter mBrowserHealthReporter;
 
     // The tab to be selected on editing mode exit.
     private Integer mTargetTabForEditingMode = null;
 
     // The animator used to toggle HomePager visibility has a race where if the HomePager is shown
     // (starting the animation), the HomePager is hidden, and the HomePager animation completes,
     // both the web content and the HomePager will be hidden. This flag is used to prevent the
     // race by determining if the web content should be hidden at the animation's end.
     private boolean mHideWebContentOnAnimationEnd = false;
 
+    private DynamicToolbar mDynamicToolbar = new DynamicToolbar();
+
     @Override
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
         if (tab == null) {
             // Only RESTORED is allowed a null tab: it's the only event that
             // isn't tied to a specific tab.
             if (msg != Tabs.TabEvents.RESTORED) {
                 throw new IllegalArgumentException("onTabChanged:" + msg + " must specify a tab.");
             }
@@ -237,19 +231,18 @@ abstract public class BrowserApp extends
                         }
                     });
                 }
                 break;
             case START:
                 if (Tabs.getInstance().isSelectedTab(tab)) {
                     invalidateOptionsMenu();
 
-                    if (isDynamicToolbarEnabled()) {
-                        // Show the toolbar.
-                        mLayerView.getLayerMarginsAnimator().showMargins(false);
+                    if (mDynamicToolbar.isEnabled()) {
+                        mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
                     }
                 }
                 break;
             case LOAD_ERROR:
             case STOP:
             case MENU_UPDATED:
                 if (Tabs.getInstance().isSelectedTab(tab)) {
                     invalidateOptionsMenu();
@@ -281,30 +274,28 @@ abstract public class BrowserApp extends
 
         // Gamepad support only exists in API-level >= 9
         if (Build.VERSION.SDK_INT >= 9 &&
             (event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
             switch (keyCode) {
                 case KeyEvent.KEYCODE_BUTTON_Y:
                     // Toggle/focus the address bar on gamepad-y button.
                     if (mViewFlipper.getVisibility() == View.VISIBLE) {
-                        if (isDynamicToolbarEnabled() && !isHomePagerVisible()) {
+                        if (mDynamicToolbar.isEnabled() && !isHomePagerVisible()) {
+                            mDynamicToolbar.setVisible(false, VisibilityTransition.ANIMATE);
                             if (mLayerView != null) {
-                                mLayerView.getLayerMarginsAnimator().hideMargins(false);
                                 mLayerView.requestFocus();
                             }
                         } else {
                             // Just focus the address bar when about:home is visible
                             // or when the dynamic toolbar isn't enabled.
                             mBrowserToolbar.requestFocusFromTouch();
                         }
                     } else {
-                        if (mLayerView != null) {
-                            mLayerView.getLayerMarginsAnimator().showMargins(false);
-                        }
+                        mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
                         mBrowserToolbar.requestFocusFromTouch();
                     }
                     return true;
                 case KeyEvent.KEYCODE_BUTTON_L1:
                     // Go back on L1
                     Tabs.getInstance().getSelectedTab().doBack();
                     return true;
                 case KeyEvent.KEYCODE_BUTTON_R1:
@@ -583,46 +574,24 @@ abstract public class BrowserApp extends
                         }
                         return new NdefMessage(new NdefRecord[] { NdefRecord.createUri(tab.getURL()) });
                     }
                 }, this);
             }
         }
 
         if (savedInstanceState != null) {
-            mDynamicToolbarEnabled = savedInstanceState.getBoolean(STATE_DYNAMIC_TOOLBAR_ENABLED);
+            mDynamicToolbar.onRestoreInstanceState(savedInstanceState);
             mHomePagerContainer.setPadding(0, savedInstanceState.getInt(STATE_ABOUT_HOME_TOP_PADDING), 0, 0);
         }
 
-        // Listen to the dynamic toolbar pref
-        mPrefObserverId = PrefsHelper.getPref(PREF_CHROME_DYNAMICTOOLBAR, new PrefsHelper.PrefHandlerBase() {
+        mDynamicToolbar.setEnabledChangedListener(new DynamicToolbar.OnEnabledChangedListener() {
             @Override
-            public void prefValue(String pref, boolean value) {
-                if (value == mDynamicToolbarEnabled) {
-                    return;
-                }
-                mDynamicToolbarEnabled = value;
-
-                ThreadUtils.postToUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        // If accessibility is enabled, the dynamic toolbar is
-                        // forced to be off.
-                        if (!mAccessibilityEnabled) {
-                            setDynamicToolbarEnabled(mDynamicToolbarEnabled);
-                        }
-                    }
-                });
-            }
-
-            @Override
-            public boolean isObserver() {
-                // We want to be notified of changes to be able to switch mode
-                // without restarting.
-                return true;
+            public void onEnabledChanged(boolean enabled) {
+                setDynamicToolbarEnabled(enabled);
             }
         });
 
         // Set the maximum bits-per-pixel the favicon system cares about.
         IconDirectoryEntry.setMaxBPP(GeckoAppShell.getScreenDepth());
     }
 
     @Override
@@ -653,16 +622,18 @@ abstract public class BrowserApp extends
     @Override
     public void onPause() {
         super.onPause();
         // Register for Prompt:ShowTop so we can foreground this activity even if it's hidden.
         registerEventListener("Prompt:ShowTop");
     }
 
     private void setDynamicToolbarEnabled(boolean enabled) {
+        ThreadUtils.assertOnUiThread();
+
         if (enabled) {
             if (mLayerView != null) {
                 mLayerView.getLayerClient().setOnMetricsChangedListener(this);
             }
             setToolbarMargin(0);
             mHomePagerContainer.setPadding(0, mViewFlipper.getHeight(), 0, 0);
         } else {
             // Immediately show the toolbar when disabling the dynamic
@@ -674,20 +645,16 @@ abstract public class BrowserApp extends
             if (mViewFlipper != null) {
                 ViewHelper.setTranslationY(mViewFlipper, 0);
             }
         }
 
         refreshToolbarHeight();
     }
 
-    private boolean isDynamicToolbarEnabled() {
-        return mDynamicToolbarEnabled && !mAccessibilityEnabled;
-    }
-
     private static boolean isAboutHome(final Tab tab) {
         return AboutPages.isAboutHome(tab.getURL());
     }
 
     @Override
     public boolean onSearchRequested() {
         enterEditingMode();
         return true;
@@ -753,34 +720,23 @@ abstract public class BrowserApp extends
             return true;
         }
 
         return false;
     }
 
     @Override
     public void setAccessibilityEnabled(boolean enabled) {
-        if (mAccessibilityEnabled == enabled) {
-            return;
-        }
-
-        // Disable the dynamic toolbar when accessibility features are enabled,
-        // and re-read the preference when they're disabled.
-        mAccessibilityEnabled = enabled;
-        if (mDynamicToolbarEnabled) {
-            setDynamicToolbarEnabled(!enabled);
-        }
+        mDynamicToolbar.setAccessibilityEnabled(enabled);
     }
 
     @Override
     public void onDestroy() {
-        if (mPrefObserverId != null) {
-            PrefsHelper.removeObserver(mPrefObserverId);
-            mPrefObserverId = null;
-        }
+        mDynamicToolbar.destroy();
+
         if (mBrowserToolbar != null)
             mBrowserToolbar.onDestroy();
 
         if (mFindInPageBar != null) {
             mFindInPageBar.onDestroy();
             mFindInPageBar = null;
         }
 
@@ -832,22 +788,18 @@ abstract public class BrowserApp extends
     }
 
     @Override
     protected void initializeChrome() {
         super.initializeChrome();
 
         mDoorHangerPopup.setAnchor(mBrowserToolbar.getDoorHangerAnchor());
 
-        // Listen to margin changes to position the toolbar correctly
-        if (isDynamicToolbarEnabled()) {
-            refreshToolbarHeight();
-            mLayerView.getLayerMarginsAnimator().showMargins(true);
-            mLayerView.getLayerClient().setOnMetricsChangedListener(this);
-        }
+        mDynamicToolbar.setLayerView(mLayerView);
+        setDynamicToolbarEnabled(mDynamicToolbar.isEnabled());
 
         // Intercept key events for gamepad shortcuts
         mLayerView.setOnKeyListener(this);
 
         // Initialize the actionbar menu items on startup for both large and small tablets
         if (HardwareUtils.isTablet()) {
             onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, null);
             invalidateOptionsMenu();
@@ -897,17 +849,17 @@ abstract public class BrowserApp extends
         // If the page has shrunk so that the toolbar no longer scrolls, make
         // sure the toolbar is visible.
         if (aMetrics.getPageHeight() <= aMetrics.getHeight()) {
             if (mDynamicToolbarCanScroll) {
                 mDynamicToolbarCanScroll = false;
                 if (mViewFlipper.getVisibility() != View.VISIBLE) {
                     ThreadUtils.postToUiThread(new Runnable() {
                         public void run() {
-                            mLayerView.getLayerMarginsAnimator().showMargins(false);
+                            mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
                         }
                     });
                 }
             }
         } else {
             mDynamicToolbarCanScroll = true;
         }
 
@@ -924,59 +876,61 @@ abstract public class BrowserApp extends
         });
 
         if (mFormAssistPopup != null)
             mFormAssistPopup.onMetricsChanged(aMetrics);
     }
 
     @Override
     public void onPanZoomStopped() {
-        if (!isDynamicToolbarEnabled() || isHomePagerVisible()) {
+        if (!mDynamicToolbar.isEnabled() || isHomePagerVisible()) {
             return;
         }
 
         // Make sure the toolbar is fully hidden or fully shown when the user
         // lifts their finger. If the page is shorter than the viewport, the
         // toolbar is always shown.
         ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
         if (metrics.getPageHeight() < metrics.getHeight()
               || metrics.marginTop >= mToolbarHeight / 2) {
-            mLayerView.getLayerMarginsAnimator().showMargins(false);
+            mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
         } else {
-            mLayerView.getLayerMarginsAnimator().hideMargins(false);
+            mDynamicToolbar.setVisible(false, VisibilityTransition.ANIMATE);
         }
     }
 
     public void refreshToolbarHeight() {
+        ThreadUtils.assertOnUiThread();
+
         int height = 0;
         if (mViewFlipper != null) {
             height = mViewFlipper.getHeight();
         }
 
-        if (!isDynamicToolbarEnabled() || isHomePagerVisible()) {
+        if (!mDynamicToolbar.isEnabled() || isHomePagerVisible()) {
             // Use aVisibleHeight here so that when the dynamic toolbar is
             // enabled, the padding will animate with the toolbar becoming
             // visible.
-            if (isDynamicToolbarEnabled()) {
+            if (mDynamicToolbar.isEnabled()) {
                 // When the dynamic toolbar is enabled, set the padding on the
                 // about:home widget directly - this is to avoid resizing the
                 // LayerView, which can cause visible artifacts.
                 mHomePagerContainer.setPadding(0, height, 0, 0);
             } else {
                 setToolbarMargin(height);
                 height = 0;
             }
         } else {
             setToolbarMargin(0);
         }
 
         if (mLayerView != null && height != mToolbarHeight) {
             mToolbarHeight = height;
             mLayerView.getLayerMarginsAnimator().setMaxMargins(0, height, 0, 0);
-            mLayerView.getLayerMarginsAnimator().showMargins(true);
+            mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
         }
     }
 
     @Override
     void toggleChrome(final boolean aShow) {
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
@@ -1345,22 +1299,22 @@ abstract public class BrowserApp extends
                                        -height);
         }
 
         mTabsPanel.prepareTabsAnimation(mMainLayoutAnimator);
         mBrowserToolbar.prepareTabsAnimation(mMainLayoutAnimator, areTabsShown());
 
         // If the tabs layout is animating onto the screen, pin the dynamic
         // toolbar.
-        if (mLayerView != null && isDynamicToolbarEnabled()) {
+        if (mDynamicToolbar.isEnabled()) {
             if (width > 0 && height > 0) {
-                mLayerView.getLayerMarginsAnimator().setMarginsPinned(true);
-                mLayerView.getLayerMarginsAnimator().showMargins(false);
+                mDynamicToolbar.setPinned(true, PinReason.RELAYOUT);
+                mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
             } else {
-                mLayerView.getLayerMarginsAnimator().setMarginsPinned(false);
+                mDynamicToolbar.setPinned(false, PinReason.RELAYOUT);
             }
         }
 
         mMainLayoutAnimator.start();
     }
 
     @Override
     public void onPropertyAnimationStart() {
@@ -1376,17 +1330,17 @@ abstract public class BrowserApp extends
         mTabsPanel.finishTabsAnimation();
 
         mMainLayoutAnimator = null;
     }
 
     @Override
     public void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
-        outState.putBoolean(STATE_DYNAMIC_TOOLBAR_ENABLED, mDynamicToolbarEnabled);
+        mDynamicToolbar.onSaveInstanceState(outState);
         outState.putInt(STATE_ABOUT_HOME_TOP_PADDING, mHomePagerContainer.getPaddingTop());
     }
 
     /**
      * Attempts to switch to an open tab with the given URL.
      *
      * @return true if we successfully switched to a tab, false otherwise.
      */
@@ -1638,19 +1592,18 @@ abstract public class BrowserApp extends
         if (mBrowserToolbar.isEditing()) {
             return;
         }
 
         if (isAboutHome(tab)) {
             final String pageId = AboutPages.getPageIdFromAboutHomeUrl(tab.getURL());
             showHomePager(pageId);
 
-            if (isDynamicToolbarEnabled()) {
-                // Show the toolbar.
-                mLayerView.getLayerMarginsAnimator().showMargins(false);
+            if (mDynamicToolbar.isEnabled()) {
+                mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
             }
         } else {
             hideHomePager();
         }
     }
 
     @Override
     public void onLocaleReady(final String locale) {
@@ -1679,18 +1632,18 @@ abstract public class BrowserApp extends
             return;
         }
 
         // Refresh toolbar height to possibly restore the toolbar padding
         refreshToolbarHeight();
 
         // Show the toolbar before hiding about:home so the
         // onMetricsChanged callback still works.
-        if (isDynamicToolbarEnabled() && mLayerView != null) {
-            mLayerView.getLayerMarginsAnimator().showMargins(true);
+        if (mDynamicToolbar.isEnabled()) {
+            mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
         }
 
         if (mHomePager == null) {
             final ViewStub homePagerStub = (ViewStub) findViewById(R.id.home_pager_stub);
             mHomePager = (HomePager) homePagerStub.inflate();
 
             final HomeBanner homeBanner = (HomeBanner) findViewById(R.id.home_banner);
             mHomePager.setBanner(homeBanner);
@@ -2057,44 +2010,45 @@ abstract public class BrowserApp extends
 
         // Scroll custom menu to the top
         if (mMenuPanel != null)
             mMenuPanel.scrollTo(0, 0);
 
         if (!mBrowserToolbar.openOptionsMenu())
             super.openOptionsMenu();
 
-        if (isDynamicToolbarEnabled() && mLayerView != null)
-            mLayerView.getLayerMarginsAnimator().showMargins(false);
+        if (mDynamicToolbar.isEnabled()) {
+            mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
+        }
     }
 
     @Override
     public void closeOptionsMenu() {
         if (!mBrowserToolbar.closeOptionsMenu())
             super.closeOptionsMenu();
     }
 
     @Override
     public void setFullScreen(final boolean fullscreen) {
         super.setFullScreen(fullscreen);
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
                 if (fullscreen) {
                     mViewFlipper.setVisibility(View.GONE);
-                    if (isDynamicToolbarEnabled()) {
-                        mLayerView.getLayerMarginsAnimator().hideMargins(true);
+                    if (mDynamicToolbar.isEnabled()) {
+                        mDynamicToolbar.setVisible(false, VisibilityTransition.IMMEDIATE);
                         mLayerView.getLayerMarginsAnimator().setMaxMargins(0, 0, 0, 0);
                     } else {
                         setToolbarMargin(0);
                     }
                 } else {
                     mViewFlipper.setVisibility(View.VISIBLE);
-                    if (isDynamicToolbarEnabled()) {
-                        mLayerView.getLayerMarginsAnimator().showMargins(true);
+                    if (mDynamicToolbar.isEnabled()) {
+                        mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
                         mLayerView.getLayerMarginsAnimator().setMaxMargins(0, mToolbarHeight, 0, 0);
                     }
                 }
             }
         });
     }
 
     @Override
@@ -2665,26 +2619,26 @@ abstract public class BrowserApp extends
     @Override
     public void startActionModeCompat(final ActionModeCompat.Callback callback) {
         // If actionMode is null, we're not currently showing one. Flip to the action mode view
         if (mActionMode == null) {
             mViewFlipper.showNext();
             LayerMarginsAnimator margins = mLayerView.getLayerMarginsAnimator();
 
             // If the toolbar is dynamic and not currently showing, just slide it in
-            if (isDynamicToolbarEnabled() && !margins.areMarginsShown()) {
+            if (mDynamicToolbar.isEnabled() && !margins.areMarginsShown()) {
                 margins.setMaxMargins(0, mViewFlipper.getHeight(), 0, 0);
-                margins.showMargins(false);
+                mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
                 mShowActionModeEndAnimation = true;
             } else {
                 // Otherwise, we animate the actionbar itself
                 mActionBar.animateIn();
             }
 
-            margins.setMarginsPinned(true);
+            mDynamicToolbar.setPinned(true, PinReason.ACTION_MODE);
         } else {
             // Otherwise, we're already showing an action mode. Just finish it and show the new one
             mActionMode.finish();
         }
 
         mActionMode = new ActionModeCompat(BrowserApp.this, callback, mActionBar);
         if (callback.onCreateActionMode(mActionMode, mActionMode.getMenu())) {
             mActionMode.invalidate();
@@ -2695,25 +2649,24 @@ abstract public class BrowserApp extends
     @Override
     public void endActionModeCompat() {
         if (mActionMode == null) {
             return;
         }
 
         mActionMode.finish();
         mActionMode = null;
-        final LayerMarginsAnimator margins = mLayerView.getLayerMarginsAnimator();
-        margins.setMarginsPinned(false);
+        mDynamicToolbar.setPinned(false, PinReason.ACTION_MODE);
 
         mViewFlipper.showPrevious();
 
         // Only slide the urlbar out if it was hidden when the action mode started
         // Don't animate hiding it so that there's no flash as we switch back to url mode
         if (mShowActionModeEndAnimation) {
-            margins.hideMargins(true);
+            mDynamicToolbar.setVisible(false, VisibilityTransition.IMMEDIATE);
             mShowActionModeEndAnimation = false;
         }
     }
 
     @Override
     protected HealthRecorder createHealthRecorder(final Context context,
                                                   final String profilePath,
                                                   final EventDispatcher dispatcher,
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/DynamicToolbar.java
@@ -0,0 +1,167 @@
+package org.mozilla.gecko;
+
+import java.util.EnumSet;
+
+import org.mozilla.gecko.PrefsHelper.PrefHandlerBase;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.os.Bundle;
+
+public class DynamicToolbar {
+    private static final String STATE_ENABLED = "dynamic_toolbar";
+    private static final String CHROME_PREF = "browser.chrome.dynamictoolbar";
+
+    // DynamicToolbar is enabled iff prefEnabled is true *and* accessibilityEnabled is false,
+    // so it is disabled by default on startup. We do not enable it until we explicitly get
+    // the pref from Gecko telling us to turn it on.
+    private volatile boolean prefEnabled;
+    private boolean accessibilityEnabled;
+
+    private final int prefObserverId;
+    private final EnumSet<PinReason> pinFlags = EnumSet.noneOf(PinReason.class);
+    private LayerView layerView;
+    private OnEnabledChangedListener enabledChangedListener;
+
+    public enum PinReason {
+        RELAYOUT,
+        ACTION_MODE
+    }
+
+    public enum VisibilityTransition {
+        IMMEDIATE,
+        ANIMATE
+    }
+
+    /**
+     * Listener for changes to the dynamic toolbar's enabled state.
+     */
+    public interface OnEnabledChangedListener {
+        /**
+         * This callback is executed on the UI thread.
+         */
+        public void onEnabledChanged(boolean enabled);
+    }
+
+    public DynamicToolbar() {
+        // Listen to the dynamic toolbar pref
+        prefObserverId = PrefsHelper.getPref(CHROME_PREF, new PrefHandler());
+    }
+
+    public void destroy() {
+        PrefsHelper.removeObserver(prefObserverId);
+    }
+
+    public void setLayerView(LayerView layerView) {
+        ThreadUtils.assertOnUiThread();
+
+        this.layerView = layerView;
+    }
+
+    public void setEnabledChangedListener(OnEnabledChangedListener listener) {
+        ThreadUtils.assertOnUiThread();
+
+        enabledChangedListener = listener;
+    }
+
+    public void onSaveInstanceState(Bundle outState) {
+        ThreadUtils.assertOnUiThread();
+
+        outState.putBoolean(STATE_ENABLED, prefEnabled);
+    }
+
+    public void onRestoreInstanceState(Bundle savedInstanceState) {
+        ThreadUtils.assertOnUiThread();
+
+        if (savedInstanceState != null) {
+            prefEnabled = savedInstanceState.getBoolean(STATE_ENABLED);
+        }
+    }
+
+    public boolean isEnabled() {
+        ThreadUtils.assertOnUiThread();
+
+        return prefEnabled && !accessibilityEnabled;
+    }
+
+    public void setAccessibilityEnabled(boolean enabled) {
+        ThreadUtils.assertOnUiThread();
+
+        if (accessibilityEnabled == enabled) {
+            return;
+        }
+
+        // Disable the dynamic toolbar when accessibility features are enabled,
+        // and re-read the preference when they're disabled.
+        accessibilityEnabled = enabled;
+        if (prefEnabled) {
+            triggerEnabledListener();
+        }
+    }
+
+    public void setVisible(boolean visible, VisibilityTransition transition) {
+        ThreadUtils.assertOnUiThread();
+
+        if (layerView == null) {
+            return;
+        }
+
+        final boolean immediate = transition.equals(VisibilityTransition.ANIMATE);
+        if (visible) {
+            layerView.getLayerMarginsAnimator().showMargins(immediate);
+        } else {
+            layerView.getLayerMarginsAnimator().hideMargins(immediate);
+        }
+    }
+
+    public void setPinned(boolean pinned, PinReason reason) {
+        ThreadUtils.assertOnUiThread();
+
+        if (layerView == null) {
+            return;
+        }
+
+        if (pinned) {
+            pinFlags.add(reason);
+        } else {
+            pinFlags.remove(reason);
+        }
+
+        layerView.getLayerMarginsAnimator().setMarginsPinned(!pinFlags.isEmpty());
+    }
+
+    private void triggerEnabledListener() {
+        if (enabledChangedListener != null) {
+            enabledChangedListener.onEnabledChanged(isEnabled());
+        }
+    }
+
+    private class PrefHandler extends PrefHandlerBase {
+        @Override
+        public void prefValue(String pref, boolean value) {
+            if (value == prefEnabled) {
+                return;
+            }
+
+            prefEnabled = value;
+
+            ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    // If accessibility is enabled, the dynamic toolbar is
+                    // forced to be off.
+                    if (!accessibilityEnabled) {
+                        triggerEnabledListener();
+                    }
+                }
+            });
+        }
+
+        @Override
+        public boolean isObserver() {
+            // We want to be notified of changes to be able to switch mode
+            // without restarting.
+            return true;
+        }
+    }
+}
\ No newline at end of file
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -127,16 +127,17 @@ gbjar.sources += [
     'db/PasswordsProvider.java',
     'db/PerProfileDatabases.java',
     'db/ReadingListProvider.java',
     'db/SQLiteBridgeContentProvider.java',
     'db/TabsProvider.java',
     'db/TransactionalProvider.java',
     'Distribution.java',
     'DoorHangerPopup.java',
+    'DynamicToolbar.java',
     'EditBookmarkDialog.java',
     'EventDispatcher.java',
     'favicons/cache/FaviconCache.java',
     'favicons/cache/FaviconCacheElement.java',
     'favicons/cache/FaviconsForURL.java',
     'favicons/decoders/FaviconDecoder.java',
     'favicons/decoders/ICODecoder.java',
     'favicons/decoders/IconDirectoryEntry.java',