Bug 979166 - Refactor dynamic toolbar code. r=lucasr
authorBrian Nicholson <bnicholson@mozilla.com>
Thu, 13 Mar 2014 12:29:39 -0700
changeset 191706 76d856e4ec61d8702bb5bb06351319823d34707c
parent 191705 21793ea94b09b2c2df3a3cefac56ef7822e5d96f
child 191707 eaabf46d60ad68421bcc9258ad9e2e1939d65b06
push id474
push userasasaki@mozilla.com
push dateMon, 02 Jun 2014 21:01:02 +0000
treeherdermozilla-release@967f4cf1b31c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslucasr
bugs979166
milestone30.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 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',