/* -*- 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.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DownloadManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.NfcEvent;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.StrictMode;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.annotation.UiThread;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.res.ResourcesCompat;
import android.support.v4.view.MenuItemCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.animation.Interpolator;
import android.widget.Button;
import android.widget.ListView;
import android.widget.ViewFlipper;

import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
import org.mozilla.gecko.Tabs.TabEvents;
import org.mozilla.gecko.activitystream.ActivityStream;
import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
import org.mozilla.gecko.adjust.AdjustBrowserAppDelegate;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.bookmarks.BookmarkEditFragment;
import org.mozilla.gecko.bookmarks.BookmarkUtils;
import org.mozilla.gecko.bookmarks.EditBookmarkTask;
import org.mozilla.gecko.cleanup.FileCleanupController;
import org.mozilla.gecko.dawn.DawnHelper;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.SuggestedSites;
import org.mozilla.gecko.delegates.BookmarkStateChangeDelegate;
import org.mozilla.gecko.delegates.BrowserAppDelegate;
import org.mozilla.gecko.delegates.OfflineTabStatusDelegate;
import org.mozilla.gecko.delegates.ScreenshotDelegate;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.distribution.DistributionStoreCallback;
import org.mozilla.gecko.dlc.DownloadContentService;
import org.mozilla.gecko.extensions.ExtensionPermissionsHelper;
import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
import org.mozilla.gecko.home.BrowserSearch;
import org.mozilla.gecko.home.HomeBanner;
import org.mozilla.gecko.home.HomeConfig;
import org.mozilla.gecko.home.HomeConfig.PanelType;
import org.mozilla.gecko.home.HomeConfigPrefsBackend;
import org.mozilla.gecko.home.HomeFragment;
import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.home.HomePanelsManager;
import org.mozilla.gecko.home.HomeScreen;
import org.mozilla.gecko.home.SearchEngine;
import org.mozilla.gecko.icons.Icons;
import org.mozilla.gecko.icons.IconsHelper;
import org.mozilla.gecko.icons.decoders.FaviconDecoder;
import org.mozilla.gecko.icons.decoders.IconDirectoryEntry;
import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
import org.mozilla.gecko.lwt.LightweightTheme;
import org.mozilla.gecko.media.VideoPlayer;
import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.menu.GeckoMenuItem;
import org.mozilla.gecko.mma.MmaDelegate;
import org.mozilla.gecko.mozglue.GeckoLoader;
import org.mozilla.gecko.mozglue.SafeIntent;
import org.mozilla.gecko.notifications.NotificationHelper;
import org.mozilla.gecko.overlays.ui.ShareDialog;
import org.mozilla.gecko.permissions.Permissions;
import org.mozilla.gecko.preferences.ClearOnShutdownPref;
import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.promotion.AddToHomeScreenPromotion;
import org.mozilla.gecko.promotion.ReaderViewBookmarkPromotion;
import org.mozilla.gecko.prompts.Prompt;
import org.mozilla.gecko.reader.ReaderModeUtils;
import org.mozilla.gecko.reader.ReadingListHelper;
import org.mozilla.gecko.reader.SavedReaderViewHelper;
import org.mozilla.gecko.restrictions.Restrictable;
import org.mozilla.gecko.restrictions.Restrictions;
import org.mozilla.gecko.search.SearchEngineManager;
import org.mozilla.gecko.switchboard.AsyncConfigLoader;
import org.mozilla.gecko.switchboard.SwitchBoard;
import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
import org.mozilla.gecko.tabqueue.TabQueueHelper;
import org.mozilla.gecko.tabqueue.TabQueuePrompt;
import org.mozilla.gecko.tabs.TabHistoryController;
import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
import org.mozilla.gecko.tabs.TabHistoryFragment;
import org.mozilla.gecko.tabs.TabHistoryPage;
import org.mozilla.gecko.tabs.TabsPanel;
import org.mozilla.gecko.telemetry.TelemetryCorePingDelegate;
import org.mozilla.gecko.telemetry.TelemetryUploadService;
import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements;
import org.mozilla.gecko.toolbar.AutocompleteHandler;
import org.mozilla.gecko.toolbar.BrowserToolbar;
import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
import org.mozilla.gecko.toolbar.PwaConfirm;
import org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt;
import org.mozilla.gecko.updater.PostUpdateHandler;
import org.mozilla.gecko.updater.UpdateServiceHelper;
import org.mozilla.gecko.util.ActivityUtils;
import org.mozilla.gecko.util.ContextUtils;
import org.mozilla.gecko.util.DrawableUtil;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GamepadUtils;
import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.IntentUtils;
import org.mozilla.gecko.util.MenuUtils;
import org.mozilla.gecko.util.PrefUtils;
import org.mozilla.gecko.util.ShortcutUtils;
import org.mozilla.gecko.util.StringUtils;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.WindowUtil;
import org.mozilla.gecko.widget.ActionModePresenter;
import org.mozilla.gecko.widget.AnchoredPopup;
import org.mozilla.gecko.widget.AnimatedProgressBar;
import org.mozilla.gecko.widget.GeckoActionProvider;
import org.mozilla.gecko.widget.SplashScreen;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;

import static org.mozilla.gecko.mma.MmaDelegate.NEW_TAB;

public class BrowserApp extends GeckoApp
                        implements ActionModePresenter,
                                   AnchoredPopup.OnVisibilityChangeListener,
                                   BookmarkEditFragment.Callbacks,
                                   BrowserSearch.OnEditSuggestionListener,
                                   BrowserSearch.OnSearchListener,
                                   DynamicToolbarAnimator.ToolbarChromeProxy,
                                   LayoutInflater.Factory,
                                   LightweightTheme.OnChangeListener,
                                   OnUrlOpenListener,
                                   OnUrlOpenInBackgroundListener,
                                   PropertyAnimator.PropertyAnimationListener,
                                   TabsPanel.TabsLayoutChangeListener,
                                   View.OnKeyListener {
    private static final String LOGTAG = "GeckoBrowserApp";

    private static final int TABS_ANIMATION_DURATION = 450;

    // Intent String extras used to specify custom Switchboard configurations.
    private static final String INTENT_KEY_SWITCHBOARD_SERVER = "switchboard-server";

    // TODO: Replace with kinto endpoint.
    private static final String SWITCHBOARD_SERVER = "https://firefox.settings.services.mozilla.com/v1/buckets/fennec/collections/experiments/records";

    private static final String STATE_ABOUT_HOME_TOP_PADDING = "abouthome_top_padding";
    private static final String STATE_ADDON_MENU_ITEM_CACHE = "menuitems_cache";
    private static final String STATE_BROWSER_ACTION_ITEM_CACHE = "browseractions_cache";

    private static final String BROWSER_SEARCH_TAG = "browser_search";

    // Request ID for startActivityForResult.
    public static final int ACTIVITY_REQUEST_PREFERENCES = 1001;
    private static final int ACTIVITY_REQUEST_TAB_QUEUE = 2001;
    public static final int ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK = 3001;
    public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS = 3002;
    public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE = 3003;
    public static final int ACTIVITY_REQUEST_TRIPLE_READERVIEW = 4001;
    public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK = 4002;
    public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE = 4003;

    public static final String ACTION_VIEW_MULTIPLE = AppConstants.ANDROID_PACKAGE_NAME + ".action.VIEW_MULTIPLE";

    @RobocopTarget
    public static final String EXTRA_SKIP_STARTPANE = "skipstartpane";
    private static final String EOL_NOTIFIED = "eol_notified";

    private BrowserSearch mBrowserSearch;
    private View mBrowserSearchContainer;

    public ViewGroup mBrowserChrome;
    public ViewFlipper mActionBarFlipper;
    public ActionModeCompatView mActionBar;
    private VideoPlayer mVideoPlayer;
    private BrowserToolbar mBrowserToolbar;
    private View doorhangerOverlay;
    // We can't name the TabStrip class because it's not included on API 9.
    private TabStripInterface mTabStrip;
    private AnimatedProgressBar mProgressView;
    private FirstrunAnimationContainer mFirstrunAnimationContainer;
    private HomeScreen mHomeScreen;
    private TabsPanel mTabsPanel;

    private boolean showSplashScreen = false;
    private SplashScreen splashScreen;
    /**
     * Container for the home screen implementation. This will be populated with any valid
     * home screen implementation (currently that is just the HomePager, but that will be extended
     * to permit further experimental replacement panels such as the activity-stream panel).
     */
    private ViewGroup mHomeScreenContainer;
    private int mCachedRecentTabsCount;
    private ActionModeCompat mActionMode;
    private TabHistoryController tabHistoryController;

    private static final int GECKO_TOOLS_MENU = -1;
    private static final int ADDON_MENU_OFFSET = 1000;
    private static final int BROWSER_ACTION_MENU_OFFSET = 10000;
    public static final String TAB_HISTORY_FRAGMENT_TAG = "tabHistoryFragment";

    // When the static action bar is shown, only the real toolbar chrome should be
    // shown when the toolbar is visible. Causing the toolbar animator to also
    // show the snapshot causes the content to shift under the users finger.
    // See: Bug 1358554
    private boolean mShowingToolbarChromeForActionBar;

    private static class MenuItemInfo implements Parcelable {
        public int id;
        public String label;
        public boolean checkable;
        public boolean checked;
        public boolean enabled = true;
        public boolean visible = true;
        public int parent;
        public boolean added;   // So we can re-add after a locale change.

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(id);
            dest.writeString(label);
            dest.writeInt(checkable ? 1 : 0);
            dest.writeInt(checked ? 1 : 0);
            dest.writeInt(enabled ? 1 : 0);
            dest.writeInt(visible ? 1 : 0);
            dest.writeInt(parent);
            dest.writeInt(added ? 1 : 0);
        }

        public static final Parcelable.Creator<MenuItemInfo> CREATOR
                = new Parcelable.Creator<MenuItemInfo>() {
            @Override
            public MenuItemInfo createFromParcel(Parcel source) {
                return new MenuItemInfo(source);
            }

            @Override
            public MenuItemInfo[] newArray(int size) {
                return new MenuItemInfo[size];
            }
        };

        private MenuItemInfo(Parcel source) {
            id = source.readInt();
            label = source.readString();
            checkable = source.readInt() != 0;
            checked = source.readInt() != 0;
            enabled = source.readInt() != 0;
            visible = source.readInt() != 0;
            parent = source.readInt();
            added = source.readInt() != 0;
        }

        public MenuItemInfo() { }
    }

    private static class BrowserActionItemInfo extends MenuItemInfo {
        public String uuid;

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeString(uuid);
        }

        public static final Parcelable.Creator<BrowserActionItemInfo> CREATOR
                = new Parcelable.Creator<BrowserActionItemInfo>() {
            @Override
            public BrowserActionItemInfo createFromParcel(Parcel source) {
                return new BrowserActionItemInfo(source);
            }

            @Override
            public BrowserActionItemInfo[] newArray(int size) {
                return new BrowserActionItemInfo[size];
            }
        };

        private BrowserActionItemInfo(Parcel source) {
            super(source);
            uuid = source.readString();
        }

        public BrowserActionItemInfo() { }
    }

    // The types of guest mode dialogs we show.
    public static enum GuestModeDialog {
        ENTERING,
        LEAVING
    }

    private ArrayList<MenuItemInfo> mAddonMenuItemsCache;
    private ArrayList<BrowserActionItemInfo> mBrowserActionItemsCache;
    private PropertyAnimator mMainLayoutAnimator;

    private static final Interpolator sTabsInterpolator = new Interpolator() {
        @Override
        public float getInterpolation(float t) {
            t -= 1.0f;
            return t * t * t * t * t + 1.0f;
        }
    };

    private FindInPageBar mFindInPageBar;
    private MediaCastingBar mMediaCastingBar;

    // We'll ask for feedback after the user launches the app this many times.
    private static final int FEEDBACK_LAUNCH_COUNT = 15;

    // Stored value of the toolbar height, so we know when it's changed.
    private int mToolbarHeight;

    private SharedPreferencesHelper mSharedPreferencesHelper;

    private ReadingListHelper mReadingListHelper;

    private AccountsHelper mAccountsHelper;

    private ExtensionPermissionsHelper mExtensionPermissionsHelper;

    // The tab to be selected on editing mode exit.
    private Integer mTargetTabForEditingMode;

    private final TabEditingState mLastTabEditingState = new TabEditingState();

    // 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;

    private final DynamicToolbar mDynamicToolbar = new DynamicToolbar();

    private final TelemetryCorePingDelegate mTelemetryCorePingDelegate = new TelemetryCorePingDelegate();

    private final List<BrowserAppDelegate> delegates = Collections.unmodifiableList(Arrays.asList(
            new AddToHomeScreenPromotion(),
            new ScreenshotDelegate(),
            new BookmarkStateChangeDelegate(),
            new ReaderViewBookmarkPromotion(),
            new PostUpdateHandler(),
            mTelemetryCorePingDelegate,
            new OfflineTabStatusDelegate(),
            new AdjustBrowserAppDelegate(mTelemetryCorePingDelegate)
    ));

    @NonNull
    private SearchEngineManager mSearchEngineManager; // Contains reference to Context - DO NOT LEAK!

    private boolean mHasResumed;

    @Override
    public View onCreateView(final View parent, final String name, final Context context, final AttributeSet attrs) {
        final View view;
        if (BrowserToolbar.class.getName().equals(name)) {
            view = BrowserToolbar.create(context, attrs);
        } else if (TabsPanel.TabsLayout.class.getName().equals(name)) {
            view = TabsPanel.createTabsLayout(context, attrs);
        } else {
            view = super.onCreateView(name, context, attrs);
        }
        return view;
    }

    @Override
    public void onTabChanged(Tab tab, TabEvents msg, String data) {
        if (!mInitialized) {
            super.onTabChanged(tab, msg, data);
            return;
        }

        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.");
            }

            final Tab selectedTab = Tabs.getInstance().getSelectedTab();
            if (selectedTab != null) {
                // After restoring the tabs we want to update the home pager immediately. Otherwise we
                // might wait for an event coming from Gecko and this can take several seconds. (Bug 1283627)
                updateHomePagerForTab(selectedTab);
            }

            return;
        }

        Log.d(LOGTAG, "BrowserApp.onTabChanged: " + tab.getId() + ": " + msg);
        switch (msg) {
            case SELECTED:
                if (mVideoPlayer.isPlaying()) {
                    mVideoPlayer.stop();
                }

                if (Tabs.getInstance().isSelectedTab(tab) && mDynamicToolbar.isEnabled()) {
                    final VisibilityTransition transition = (tab.getShouldShowToolbarWithoutAnimationOnFirstSelection()) ?
                            VisibilityTransition.IMMEDIATE : VisibilityTransition.ANIMATE;
                    mDynamicToolbar.setVisible(true, transition);

                    // The first selection has happened - reset the state.
                    tab.setShouldShowToolbarWithoutAnimationOnFirstSelection(false);
                }
                // fall through
            case LOCATION_CHANGE:
                if (Tabs.getInstance().isSelectedTab(tab)) {
                    updateHomePagerForTab(tab);
                }

                if (mShowingToolbarChromeForActionBar) {
                    mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
                    mShowingToolbarChromeForActionBar = false;
                }
                break;
            case START:
                if (Tabs.getInstance().isSelectedTab(tab)) {
                    invalidateOptionsMenu();

                    if (mDynamicToolbar.isEnabled()) {
                        mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
                    }
                }
                break;
            case LOAD_ERROR:
            case STOP:
            case MENU_UPDATED:
                if (Tabs.getInstance().isSelectedTab(tab)) {
                    invalidateOptionsMenu();
                }
                break;
            case PAGE_SHOW:
                tab.loadFavicon();
                break;
            case UNSELECTED:
                // We receive UNSELECTED immediately after the SELECTED listeners run
                // so we are ensured that the unselectedTabEditingText has not changed.
                if (tab.isEditing()) {
                    // Copy to avoid constructing new objects.
                    tab.getEditingState().copyFrom(mLastTabEditingState);
                }
                break;
            case START_EDITING:
                enterEditingMode();
                break;
        }

        if (HardwareUtils.isTablet() && msg == TabEvents.SELECTED) {
            updateEditingModeForTab(tab);
        }

        super.onTabChanged(tab, msg, data);
    }

    private void updateEditingModeForTab(final Tab selectedTab) {
        // (bug 1086983 comment 11) Because the tab may be selected from the gecko thread and we're
        // running this code on the UI thread, the selected tab argument may not still refer to the
        // selected tab. However, that means this code should be run again and the initial state
        // changes will be overridden. As an optimization, we can skip this update, but it may have
        // unknown side-effects so we don't.
        if (!Tabs.getInstance().isSelectedTab(selectedTab)) {
            Log.w(LOGTAG, "updateEditingModeForTab: Given tab is expected to be selected tab");
        }

        saveTabEditingState(mLastTabEditingState);

        if (selectedTab.isEditing()) {
            enterEditingMode();
            restoreTabEditingState(selectedTab.getEditingState());
        } else {
            mBrowserToolbar.cancelEdit();
        }
    }

    private void saveTabEditingState(final TabEditingState editingState) {
        mBrowserToolbar.saveTabEditingState(editingState);
        editingState.setIsBrowserSearchShown(mBrowserSearch.getUserVisibleHint());
    }

    private void restoreTabEditingState(final TabEditingState editingState) {
        mBrowserToolbar.restoreTabEditingState(editingState);

        // Since changing the editing text will show/hide browser search, this
        // must be called after we restore the editing state in the edit text View.
        if (editingState.isBrowserSearchShown()) {
            showBrowserSearch();
        } else {
            hideBrowserSearch();
        }
    }

    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
        if (AndroidGamepadManager.handleKeyEvent(event)) {
            return true;
        }

        // Global onKey handler. This is called if the focused UI doesn't
        // handle the key event, and before Gecko swallows the events.
        if (event.getAction() != KeyEvent.ACTION_DOWN) {
            return false;
        }

        if ((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 (mBrowserChrome.getVisibility() == View.VISIBLE) {
                        if (mDynamicToolbar.isEnabled() && !isHomePagerVisible()) {
                            mDynamicToolbar.setVisible(false, VisibilityTransition.ANIMATE);
                            if (mLayerView != null) {
                                mLayerView.requestFocus();
                            }
                        } else {
                            // Just focus the address bar when about:home is visible
                            // or when the dynamic toolbar isn't enabled.
                            mBrowserToolbar.requestFocusFromTouch();
                        }
                    } else {
                        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:
                    // Go forward on R1
                    Tabs.getInstance().getSelectedTab().doForward();
                    return true;
            }
        }

        // Check if this was a shortcut. Meta keys exists only on 11+.
        final Tab tab = Tabs.getInstance().getSelectedTab();
        if (tab != null && event.isCtrlPressed()) {
            switch (keyCode) {
                case KeyEvent.KEYCODE_LEFT_BRACKET:
                    tab.doBack();
                    return true;

                case KeyEvent.KEYCODE_RIGHT_BRACKET:
                    tab.doForward();
                    return true;

                case KeyEvent.KEYCODE_R:
                    tab.doReload(event.isShiftPressed() ? true : false);
                    return true;

                case KeyEvent.KEYCODE_PERIOD:
                    tab.doStop();
                    return true;

                case KeyEvent.KEYCODE_T:
                    addTab();
                    return true;

                case KeyEvent.KEYCODE_W:
                    Tabs.getInstance().closeTab(tab);
                    return true;

                case KeyEvent.KEYCODE_F:
                    mFindInPageBar.show(mBrowserToolbar.isPrivateMode());
                return true;
            }
        }

        return false;
    }

    private Runnable mCheckLongPress;
    {
        // Only initialise the runnable if we are >= N.
        // See onKeyDown() for more details of the back-button long-press workaround
        if (!Versions.preN) {
            mCheckLongPress = new Runnable() {
                public void run() {
                    handleBackLongPress();
                }
            };
        }
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        // Bug 1304688: Android N has broken passing onKeyLongPress events for the back button, so we
        // instead copy the long-press-handler technique from Android's KeyButtonView.
        // - For short presses, we cancel the callback in onKeyUp
        // - For long presses, the normal keypress is marked as cancelled, hence won't be handled elsewhere
        //   (but Android still provides the haptic feedback), and the runnable is run.
        if (!Versions.preN &&
                keyCode == KeyEvent.KEYCODE_BACK) {
            ThreadUtils.getUiHandler().removeCallbacks(mCheckLongPress);
            ThreadUtils.getUiHandler().postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
        }

        if (!mBrowserToolbar.isEditing() && onKey(null, keyCode, event)) {
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (!Versions.preN &&
                keyCode == KeyEvent.KEYCODE_BACK) {
            ThreadUtils.getUiHandler().removeCallbacks(mCheckLongPress);
        }

        if (AndroidGamepadManager.handleKeyEvent(event)) {
            return true;
        }
        return super.onKeyUp(keyCode, event);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        final Context appContext = getApplicationContext();

        showSplashScreen = true;

        boolean supported = HardwareUtils.isSupportedSystem();
        if (supported) {
            GeckoLoader.loadMozGlue(appContext);
            supported = GeckoLoader.neonCompatible();
        }
        if (!supported) {
            // This build does not support the Android version of the device; Exit early.
            super.onCreate(savedInstanceState);
            return;
        }

        final SafeIntent intent = new SafeIntent(getIntent());
        final boolean isInAutomation = IntentUtils.getIsInAutomationFromEnvironment(intent);

        GeckoProfile.setIntentArgs(intent.getStringExtra("args"));

        if (!isInAutomation && AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE) {
            // Kick off download of app content as early as possible so that in the best case it's
            // available before the user starts using the browser.
            DownloadContentService.startStudy(this);
        }

        // This has to be prepared prior to calling GeckoApp.onCreate, because
        // widget code and BrowserToolbar need it, and they're created by the
        // layout, which GeckoApp takes care of.
        final GeckoApplication app = (GeckoApplication) getApplication();
        app.prepareLightweightTheme();

        super.onCreate(savedInstanceState);

        initSwitchboard(this, intent, isInAutomation);
        initTelemetryUploader(isInAutomation);

        mBrowserChrome = (ViewGroup) findViewById(R.id.browser_chrome);
        mActionBarFlipper = (ViewFlipper) findViewById(R.id.browser_actionbar);
        mActionBar = (ActionModeCompatView) findViewById(R.id.actionbar);

        mVideoPlayer = (VideoPlayer) findViewById(R.id.video_player);
        mVideoPlayer.setFullScreenListener(new VideoPlayer.FullScreenListener() {
            @Override
            public void onFullScreenChanged(boolean fullScreen) {
                mVideoPlayer.setFullScreen(fullScreen);
                setFullScreen(fullScreen);
            }
        });

        mBrowserToolbar = (BrowserToolbar) findViewById(R.id.browser_toolbar);
        mBrowserToolbar.setTouchEventInterceptor(new TouchEventInterceptor() {
            @Override
            public boolean onInterceptTouchEvent(View view, MotionEvent event) {
                // Manually dismiss text selection bar if it's not overlaying the toolbar.
                mTextSelection.dismiss();
                return false;
            }

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return false;
            }
        });

        // If the activity is being restored, the add-ons menu item cache only needs restoring if
        // Gecko is already running. Otherwise, we'll simply catch the corresponding events when
        // Gecko and the add-ons are starting up.
        if (savedInstanceState != null && mIsRestoringActivity) {
            mAddonMenuItemsCache = savedInstanceState.getParcelableArrayList(STATE_ADDON_MENU_ITEM_CACHE);
            mBrowserActionItemsCache = savedInstanceState.getParcelableArrayList(STATE_BROWSER_ACTION_ITEM_CACHE);
        }

        app.getLightweightTheme().addListener(this);

        mProgressView = (AnimatedProgressBar) findViewById(R.id.page_progress);
        mDynamicToolbar.setLayerView(mLayerView);
        mProgressView.setDynamicToolbar(mDynamicToolbar);
        mBrowserToolbar.setProgressBar(mProgressView);

        // Initialize Tab History Controller.
        tabHistoryController = new TabHistoryController(new OnShowTabHistory() {
            @Override
            public void onShowHistory(final List<TabHistoryPage> historyPageList, final int toIndex) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (BrowserApp.this.isFinishing()) {
                            // TabHistoryController is rather slow - and involves calling into Gecko
                            // to retrieve tab history. That means there can be a significant
                            // delay between the back-button long-press, and onShowHistory()
                            // being called. Hence we need to guard against the Activity being
                            // shut down (in which case trying to perform UI changes, such as showing
                            // fragments below, will crash).
                            return;
                        }

                        final TabHistoryFragment fragment = TabHistoryFragment.newInstance(historyPageList, toIndex);
                        final FragmentManager fragmentManager = getSupportFragmentManager();
                        GeckoAppShell.getHapticFeedbackDelegate().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
                        fragment.show(R.id.tab_history_panel, fragmentManager.beginTransaction(), TAB_HISTORY_FRAGMENT_TAG);
                    }
                });
            }
        });
        mBrowserToolbar.setTabHistoryController(tabHistoryController);

        final String action = intent.getAction();
        if (Intent.ACTION_VIEW.equals(action)) {
            // Show the target URL immediately in the toolbar.
            mBrowserToolbar.setTitle(intent.getDataString());

            showTabQueuePromptIfApplicable(intent);
        } else if (ACTION_VIEW_MULTIPLE.equals(action) && savedInstanceState == null) {
            // We only want to handle this intent if savedInstanceState is null. In the case where
            // savedInstanceState is not null this activity is being re-created and we already
            // opened tabs for the URLs the last time. Our session store will take care of restoring
            // them.
            openMultipleTabsFromIntent(intent);
        } else if (GuestSession.NOTIFICATION_INTENT.equals(action)) {
            GuestSession.onNotificationIntentReceived(this);
        } else if (TabQueueHelper.LOAD_URLS_ACTION.equals(action)) {
            Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, "tabqueue");
        }

        if (HardwareUtils.isTablet()) {
            mTabStrip = (TabStripInterface) (((ViewStub) findViewById(R.id.tablet_tab_strip)).inflate());
        }

        ((GeckoApp.MainLayout) mMainLayout).setTouchEventInterceptor(new HideOnTouchListener());
        ((GeckoApp.MainLayout) mMainLayout).setMotionEventInterceptor(new MotionEventInterceptor() {
            @Override
            public boolean onInterceptMotionEvent(View view, MotionEvent event) {
                // If we get a gamepad panning MotionEvent while the focus is not on the layerview,
                // put the focus on the layerview and carry on
                if (mLayerView != null && !mLayerView.hasFocus() && GamepadUtils.isPanningControl(event)) {
                    if (mHomeScreen == null) {
                        return false;
                    }

                    if (isHomePagerVisible()) {
                        mLayerView.requestFocus();
                    } else {
                        mHomeScreen.requestFocus();
                    }
                }
                return false;
            }
        });

        mHomeScreenContainer = (ViewGroup) findViewById(R.id.home_screen_container);

        mBrowserSearchContainer = findViewById(R.id.search_container);
        mBrowserSearch = (BrowserSearch) getSupportFragmentManager().findFragmentByTag(BROWSER_SEARCH_TAG);
        if (mBrowserSearch == null) {
            mBrowserSearch = BrowserSearch.newInstance();
            mBrowserSearch.setUserVisibleHint(false);
        }

        setBrowserToolbarListeners();

        mFindInPageBar = (FindInPageBar) findViewById(R.id.find_in_page);
        mMediaCastingBar = (MediaCastingBar) findViewById(R.id.media_casting);

        doorhangerOverlay = findViewById(R.id.doorhanger_overlay);

        EventDispatcher.getInstance().registerGeckoThreadListener(this,
            "Search:Keyword",
            null);

        EventDispatcher.getInstance().registerUiThreadListener(this,
            "Accessibility:Enabled",
            "Menu:Open",
            "Menu:Update",
            "Menu:Add",
            "Menu:Remove",
            "Menu:AddBrowserAction",
            "Menu:RemoveBrowserAction",
            "Menu:UpdateBrowserAction",
            "LightweightTheme:Update",
            "Tab:Added",
            "Video:Play",
            "CharEncoding:Data",
            "CharEncoding:State",
            "Settings:Show",
            "Updater:Launch",
            "Sanitize:Finished",
            "Sanitize:OpenTabs",
            null);

        EventDispatcher.getInstance().registerBackgroundThreadListener(this,
            "Experiments:GetActive",
            "Experiments:SetOverride",
            "Experiments:ClearOverride",
            "Favicon:Request",
            "Feedback:MaybeLater",
            "Sanitize:ClearHistory",
            "Sanitize:ClearSyncedTabs",
            "Telemetry:Gather",
            "Download:AndroidDownloadManager",
            "Website:AppInstalled",
            "Website:AppInstallFailed",
            "Website:Metadata",
            null);

        getAppEventDispatcher().registerUiThreadListener(this, "Prompt:ShowTop");

        final GeckoProfile profile = getProfile();

        // We want to upload the telemetry core ping as soon after startup as possible. It relies on the
        // Distribution being initialized. If you move this initialization, ensure it plays well with telemetry.
        final Distribution distribution = Distribution.init(getApplicationContext());
        distribution.addOnDistributionReadyCallback(
                new DistributionStoreCallback(getApplicationContext(), profile.getName()));

        mSearchEngineManager = new SearchEngineManager(this, distribution);

        // Init suggested sites engine in BrowserDB.
        final SuggestedSites suggestedSites = new SuggestedSites(appContext, distribution);
        final BrowserDB db = BrowserDB.from(profile);
        db.setSuggestedSites(suggestedSites);

        mSharedPreferencesHelper = new SharedPreferencesHelper(appContext);
        mReadingListHelper = new ReadingListHelper(appContext, profile);
        mAccountsHelper = new AccountsHelper(appContext, profile);
        mExtensionPermissionsHelper = new ExtensionPermissionsHelper(this);

        if (AppConstants.MOZ_ANDROID_BEAM) {
            NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
            if (nfc != null) {
                nfc.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
                    @Override
                    public NdefMessage createNdefMessage(NfcEvent event) {
                        Tab tab = Tabs.getInstance().getSelectedTab();
                        if (tab == null || tab.isPrivate()) {
                            return null;
                        }
                        return new NdefMessage(new NdefRecord[] { NdefRecord.createUri(tab.getURL()) });
                    }
                }, this);
            }
        }

        if (savedInstanceState != null) {
            mDynamicToolbar.onRestoreInstanceState(savedInstanceState);
            mHomeScreenContainer.setPadding(0, savedInstanceState.getInt(STATE_ABOUT_HOME_TOP_PADDING), 0, 0);
        }

        mDynamicToolbar.setEnabledChangedListener(new DynamicToolbar.OnEnabledChangedListener() {
            @Override
            public void onEnabledChanged(boolean enabled) {
                setDynamicToolbarEnabled(enabled);
            }
        });

        // Set the maximum bits-per-pixel the favicon system cares about.
        IconDirectoryEntry.setMaxBPP(GeckoAppShell.getScreenDepth());

        // The update service is enabled for RELEASE_OR_BETA, which includes the release and beta channels.
        // However, no updates are served.  Therefore, we don't trust the update service directly, and
        // try to avoid prompting unnecessarily. See Bug 1232798.
        if (!AppConstants.RELEASE_OR_BETA && UpdateServiceHelper.isUpdaterEnabled(this)) {
            Permissions.from(this)
                       .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                       .doNotPrompt()
                       .andFallback(new Runnable() {
                           @Override
                           public void run() {
                               showUpdaterPermissionSnackbar();
                           }
                       })
                      .run();
        }

        for (final BrowserAppDelegate delegate : delegates) {
            delegate.onCreate(this, savedInstanceState);
        }

        // We want to get an understanding of how our user base is spread (bug 1221646).
        final String installerPackageName = getPackageManager().getInstallerPackageName(getPackageName());
        Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, TelemetryContract.Method.SYSTEM, "installer_" + installerPackageName);
    }

    /**
     * Initializes the default Switchboard URLs the first time.
     * @param intent
     */
    private void initSwitchboard(final Context context, final SafeIntent intent, final boolean isInAutomation) {
        if (isInAutomation) {
            Log.d(LOGTAG, "Switchboard disabled - in automation");
            return;
        } else if (!AppConstants.MOZ_SWITCHBOARD) {
            Log.d(LOGTAG, "Switchboard compile-time disabled");
            return;
        }

        final String serverExtra = intent.getStringExtra(INTENT_KEY_SWITCHBOARD_SERVER);
        final String serverUrl = TextUtils.isEmpty(serverExtra) ? SWITCHBOARD_SERVER : serverExtra;
        new AsyncConfigLoader(context, serverUrl) {
            @Override
            protected Void doInBackground(Void... params) {
                super.doInBackground(params);
                SwitchBoard.loadConfig(context, serverUrl);
                if (SwitchBoard.isInExperiment(context, Experiments.LEANPLUM) &&
                        GeckoPreferences.getBooleanPref(context, GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true)) {
                    // Do LeanPlum start/init here
                    MmaDelegate.init(BrowserApp.this);
                }
                return null;
            }
        }.execute();
    }

    private static void initTelemetryUploader(final boolean isInAutomation) {
        TelemetryUploadService.setDisabled(isInAutomation);
    }

    private void showUpdaterPermissionSnackbar() {
        SnackbarBuilder.SnackbarCallback allowCallback = new SnackbarBuilder.SnackbarCallback() {
            @Override
            public void onClick(View v) {
                Permissions.from(BrowserApp.this)
                        .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                        .run();
            }
        };

        SnackbarBuilder.builder(this)
                .message(R.string.updater_permission_text)
                .duration(Snackbar.LENGTH_INDEFINITE)
                .action(R.string.updater_permission_allow)
                .callback(allowCallback)
                .buildAndShow();
    }

    private void conditionallyNotifyEOL() {
        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
        try {
            final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
            if (!prefs.contains(EOL_NOTIFIED)) {

                // Launch main App to load SUMO url on EOL notification.
                final String link = getString(R.string.eol_notification_url,
                                              AppConstants.MOZ_APP_VERSION,
                                              AppConstants.OS_TARGET,
                                              Locales.getLanguageTag(Locale.getDefault()));

                final Intent intent = new Intent(Intent.ACTION_VIEW);
                intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
                intent.setData(Uri.parse(link));
                final PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

                final Notification notification = new NotificationCompat.Builder(this)
                        .setContentTitle(getString(R.string.eol_notification_title))
                        .setContentText(getString(R.string.eol_notification_summary))
                        .setSmallIcon(R.drawable.ic_status_logo)
                        .setAutoCancel(true)
                        .setContentIntent(pendingIntent)
                        .build();

                final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
                final int notificationID = EOL_NOTIFIED.hashCode();
                notificationManager.notify(notificationID, notification);

                GeckoSharedPrefs.forProfile(this)
                                .edit()
                                .putBoolean(EOL_NOTIFIED, true)
                                .apply();
            }
        } finally {
            StrictMode.setThreadPolicy(savedPolicy);
        }
    }

    /**
     * Code to actually show the first run pager, separated
     * for distribution purposes.
     */
    @UiThread
    private void checkFirstrunInternal() {
        showFirstrunPager();

        if (HardwareUtils.isTablet()) {
            mTabStrip.setOnTabChangedListener(new TabStripInterface.OnTabAddedOrRemovedListener() {
                @Override
                public void onTabChanged() {
                    hideFirstrunPager(TelemetryContract.Method.BUTTON);
                    mTabStrip.setOnTabChangedListener(null);
                }
            });
        }
    }

    /**
     * Check and show the firstrun pane if the browser has never been launched and
     * is not opening an external link from another application.
     *
     * @param context Context of application; used to show firstrun pane if appropriate
     * @param intent Intent that launched this activity
     */
    private void checkFirstrun(Context context, SafeIntent intent) {
        if (getProfile().inGuestMode()) {
            // We do not want to show any first run tour for guest profiles.
            return;
        }

        if (intent.getBooleanExtra(EXTRA_SKIP_STARTPANE, false)) {
            // Note that we don't set the pref, so subsequent launches can result
            // in the firstrun pane being shown.
            return;
        }
        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();

        try {
            final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);

            if (prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED_OLD, true) &&
                prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, true)) {
                showSplashScreen = false;
                if (!Intent.ACTION_VIEW.equals(intent.getAction())) {
                    // Check to see if a distribution has turned off the first run pager.
                    final Distribution distribution = Distribution.getInstance(BrowserApp.this);
                    if (!distribution.shouldWaitForSystemDistribution()) {
                        checkFirstrunInternal();
                    } else {
                        distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
                            @Override
                            public void distributionNotFound() {
                                ThreadUtils.postToUiThread(new Runnable() {
                                    @Override
                                    public void run() {
                                        checkFirstrunInternal();
                                    }
                                });
                            }

                            @Override
                            public void distributionFound(final Distribution distribution) {
                                // Check preference again in case distribution turned it off.
                                if (prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, true)) {
                                    ThreadUtils.postToUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            checkFirstrunInternal();
                                        }
                                    });
                                }
                            }

                            @Override
                            public void distributionArrivedLate(final Distribution distribution) {
                            }
                        });
                    }
                }

                // Don't bother trying again to show the v1 minimal first run.
                prefs.edit().putBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, false).apply();

                // We have no intention of stopping this session. The FIRSTRUN session
                // ends when the browsing session/activity has ended. All events
                // during firstrun will be tagged as FIRSTRUN.
                Telemetry.startUISession(TelemetryContract.Session.FIRSTRUN);
            }
        } finally {
            StrictMode.setThreadPolicy(savedPolicy);
        }
    }

    private Class<?> getMediaPlayerManager() {
        if (AppConstants.MOZ_MEDIA_PLAYER) {
            try {
                return Class.forName("org.mozilla.gecko.MediaPlayerManager");
            } catch (Exception ex) {
                // Ignore failures
                Log.e(LOGTAG, "No native casting support", ex);
            }
        }

        return null;
    }

    @Override
    public void onBackPressed() {
        if (mTextSelection.dismiss()) {
            return;
        }

        if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
            super.onBackPressed();
            return;
        }

        if (mBrowserToolbar.onBackPressed()) {
            return;
        }

        if (mActionMode != null) {
            endActionMode();
            return;
        }

        if (hideFirstrunPager(TelemetryContract.Method.BACK)) {
            return;
        }

        if (mVideoPlayer.isFullScreen()) {
            mVideoPlayer.setFullScreen(false);
            setFullScreen(false);
            return;
        }

        if (mVideoPlayer.isPlaying()) {
            mVideoPlayer.stop();
            return;
        }

        super.onBackPressed();
    }

    @Override
    public void onAttachedToWindow() {
        final SafeIntent intent = new SafeIntent(getIntent());

        // We can't show the first run experience until Gecko has finished initialization (bug 1077583).
        checkFirstrun(this, intent);

        if (!IntentUtils.getIsInAutomationFromEnvironment(intent)) {
            DawnHelper.conditionallyNotifyDawn(this);
        }
    }

    @Override
    protected void processTabQueue() {
        if (TabQueueHelper.TAB_QUEUE_ENABLED && mInitialized) {
            ThreadUtils.postToBackgroundThread(new Runnable() {
                @Override
                public void run() {
                    if (TabQueueHelper.shouldOpenTabQueueUrls(BrowserApp.this)) {
                        openQueuedTabs();
                    }
                }
            });
        }
    }

    @Override
    protected void openQueuedTabs() {
        ThreadUtils.assertNotOnUiThread();

        int queuedTabCount = TabQueueHelper.getTabQueueLength(BrowserApp.this);

        Telemetry.addToHistogram("FENNEC_TABQUEUE_QUEUESIZE", queuedTabCount);
        Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "tabqueue-delayed");

        TabQueueHelper.openQueuedUrls(BrowserApp.this, getProfile(), TabQueueHelper.FILE_NAME, false);

        // If there's more than one tab then also show the tabs panel.
        if (queuedTabCount > 1) {
            ThreadUtils.postToUiThread(new Runnable() {
                @Override
                public void run() {
                    showNormalTabs();
                }
            });
        }
    }

    private void openMultipleTabsFromIntent(final SafeIntent intent) {
        final List<String> urls = intent.getStringArrayListExtra("urls");
        if (urls != null) {
            openUrls(urls);
        }
    }

    @Override
    public void onResume() {
        super.onResume();

        if (mIsAbortingAppLaunch) {
            return;
        }

        if (!mHasResumed) {
            getAppEventDispatcher().unregisterUiThreadListener(this, "Prompt:ShowTop");
            mHasResumed = true;
        }

        processTabQueue();

        for (BrowserAppDelegate delegate : delegates) {
            delegate.onResume(this);
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        if (mIsAbortingAppLaunch) {
            return;
        }

        if (mHasResumed) {
            // Register for Prompt:ShowTop so we can foreground this activity even if it's hidden.
            getAppEventDispatcher().registerUiThreadListener(this, "Prompt:ShowTop");
            mHasResumed = false;
        }

        for (BrowserAppDelegate delegate : delegates) {
            delegate.onPause(this);
        }
    }

    @Override
    public void onRestart() {
        super.onRestart();
        if (mIsAbortingAppLaunch) {
            return;
        }

        for (final BrowserAppDelegate delegate : delegates) {
            delegate.onRestart(this);
        }
    }

    @Override
    public void onStart() {
        super.onStart();
        if (mIsAbortingAppLaunch) {
            return;
        }

        // Queue this work so that the first launch of the activity doesn't
        // trigger profile init too early.
        ThreadUtils.postToBackgroundThread(new Runnable() {
            @Override
            public void run() {
                final GeckoProfile profile = getProfile();
                if (profile.inGuestMode()) {
                    GuestSession.showNotification(BrowserApp.this);
                } else {
                    // If we're restarting, we won't destroy the activity.
                    // Make sure we remove any guest notifications that might
                    // have been shown.
                    GuestSession.hideNotification(BrowserApp.this);
                }

                // It'd be better to launch this once, in onCreate, but there's ambiguity for when the
                // profile is created so we run here instead. Don't worry, call start short-circuits pretty fast.
                final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(BrowserApp.this, profile.getName());
                FileCleanupController.startIfReady(BrowserApp.this, sharedPrefs, profile.getDir().getAbsolutePath());
            }
        });

        for (final BrowserAppDelegate delegate : delegates) {
            delegate.onStart(this);
        }
    }

    @Override
    public void onStop() {
        super.onStop();
        if (mIsAbortingAppLaunch) {
            return;
        }

        // We only show the guest mode notification when our activity is in the foreground.
        GuestSession.hideNotification(this);

        for (final BrowserAppDelegate delegate : delegates) {
            delegate.onStop(this);
        }

        onAfterStop();
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);

        // Sending a message to the toolbar when the browser window gains focus
        // This is needed for qr code input
        if (hasFocus) {
            mBrowserToolbar.onParentFocus();
        }
    }

    private void setBrowserToolbarListeners() {
        mBrowserToolbar.setOnActivateListener(new BrowserToolbar.OnActivateListener() {
            @Override
            public void onActivate() {
                enterEditingMode();
            }
        });

        mBrowserToolbar.setOnCommitListener(new BrowserToolbar.OnCommitListener() {
            @Override
            public void onCommit() {
                commitEditingMode();
            }
        });

        mBrowserToolbar.setOnDismissListener(new BrowserToolbar.OnDismissListener() {
            @Override
            public void onDismiss() {
                mBrowserToolbar.cancelEdit();
            }
        });

        mBrowserToolbar.setOnFilterListener(new BrowserToolbar.OnFilterListener() {
            @Override
            public void onFilter(String searchText, AutocompleteHandler handler) {
                filterEditingMode(searchText, handler);
            }
        });

        mBrowserToolbar.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (isHomePagerVisible()) {
                    mHomeScreen.onToolbarFocusChange(hasFocus);
                }
            }
        });

        mBrowserToolbar.setOnStartEditingListener(new BrowserToolbar.OnStartEditingListener() {
            @Override
            public void onStartEditing() {
                final Tab selectedTab = Tabs.getInstance().getSelectedTab();
                if (selectedTab != null) {
                    selectedTab.setIsEditing(true);
                }

                // Temporarily disable doorhanger notifications.
                if (mDoorHangerPopup != null) {
                    mDoorHangerPopup.disable();
                }
            }
        });

        mBrowserToolbar.setOnStopEditingListener(new BrowserToolbar.OnStopEditingListener() {
            @Override
            public void onStopEditing() {
                final Tab selectedTab = Tabs.getInstance().getSelectedTab();
                if (selectedTab != null) {
                    selectedTab.setIsEditing(false);
                }

                selectTargetTabForEditingMode();

                // Since the underlying LayerView is set visible in hideHomePager, we would
                // ordinarily want to call it first. However, hideBrowserSearch changes the
                // visibility of the HomePager and hideHomePager will take no action if the
                // HomePager is hidden, so we want to call hideBrowserSearch to restore the
                // HomePager visibility first.
                hideBrowserSearch();
                hideHomePager();

                // Re-enable doorhanger notifications. They may trigger on the selected tab above.
                if (mDoorHangerPopup != null) {
                    mDoorHangerPopup.enable();
                }
            }
        });

        // Intercept key events for gamepad shortcuts
        mBrowserToolbar.setOnKeyListener(this);
    }

    private void setDynamicToolbarEnabled(boolean enabled) {
        ThreadUtils.assertOnUiThread();

        if (mLayerView != null) {
            if (enabled) {
                 mDynamicToolbar.setPinned(false, PinReason.DISABLED);
            } else {
               // Immediately show the toolbar when disabling the dynamic
               // toolbar.
                mDynamicToolbar.setPinned(true, PinReason.DISABLED);
                mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
            }
        }

        refreshToolbarHeight();
    }

    private static boolean isAboutHome(final Tab tab) {
        return AboutPages.isAboutHome(tab.getURL());
    }

    @Override
    public boolean onSearchRequested() {
        enterEditingMode();
        return true;
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        final int itemId = item.getItemId();
        if (itemId == R.id.pasteandgo) {
            hideFirstrunPager(TelemetryContract.Method.CONTEXT_MENU);

            String text = Clipboard.getText(this);
            if (!TextUtils.isEmpty(text)) {
                loadUrlOrKeywordSearch(text);
                Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.CONTEXT_MENU);
                Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "pasteandgo");
            }
            return true;
        }

        if (itemId == R.id.paste) {
            String text = Clipboard.getText(this);
            if (!TextUtils.isEmpty(text)) {
                enterEditingMode(text);
                showBrowserSearch();
                mBrowserSearch.filter(text, null);
                Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "paste");
            }
            return true;
        }

        if (itemId == R.id.subscribe) {
            // This can be selected from either the browser menu or the contextmenu, depending on the size and version (v11+) of the phone.
            Tab tab = Tabs.getInstance().getSelectedTab();
            if (tab != null && tab.hasFeeds()) {
                final GeckoBundle args = new GeckoBundle(1);
                args.putInt("tabId", tab.getId());
                EventDispatcher.getInstance().dispatch("Feeds:Subscribe", args);
            }
            return true;
        }

        if (itemId == R.id.add_search_engine) {
            // This can be selected from either the browser menu or the contextmenu, depending on the size and version (v11+) of the phone.
            Tab tab = Tabs.getInstance().getSelectedTab();
            if (tab != null && tab.hasOpenSearch()) {
                final GeckoBundle args = new GeckoBundle(1);
                args.putInt("tabId", tab.getId());
                EventDispatcher.getInstance().dispatch("SearchEngines:Add", args);
            }
            return true;
        }

        if (itemId == R.id.copyurl) {
            Tab tab = Tabs.getInstance().getSelectedTab();
            if (tab != null) {
                String url = ReaderModeUtils.stripAboutReaderUrl(tab.getURL());
                if (url != null) {
                    Clipboard.setText(this, url);
                    Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "copyurl");
                }
            }
            return true;
        }

        if (itemId == R.id.pin_to_top_sites) {
            final Tab selectedTab = Tabs.getInstance().getSelectedTab();
            if (selectedTab != null) {
                ThreadUtils.postToBackgroundThread(new Runnable() {
                    @Override
                    public void run() {
                        final ActivityStreamTelemetry.Extras.Builder telemetryExtraBuilder = ActivityStreamTelemetry.Extras.builder();
                        final BrowserDB db = BrowserDB.from(BrowserApp.this);
                        final ContentResolver cr = getContentResolver();
                        final String url = selectedTab.getURL();

                        final @StringRes int snackbarText;
                        if (!db.isPinnedForAS(cr, url)) {
                            db.pinSiteForAS(getContentResolver(), url, selectedTab.getTitle());
                            snackbarText = R.string.pinned_page_to_top_sites;
                            telemetryExtraBuilder.set(ActivityStreamTelemetry.Contract.ITEM, ActivityStreamTelemetry.Contract.ITEM_PIN);
                        } else {
                            db.unpinSiteForAS(getContentResolver(), url);
                            snackbarText = R.string.unpinned_page_from_top_sites;
                            telemetryExtraBuilder.set(ActivityStreamTelemetry.Contract.ITEM, ActivityStreamTelemetry.Contract.ITEM_UNPIN);
                        }

                        SnackbarBuilder.builder(BrowserApp.this)
                                .message(snackbarText)
                                .buildAndShow();

                        Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, // via browser menu.
                                telemetryExtraBuilder.build());
                    }
                });
            }
            return true;
        }

        if (itemId == R.id.add_to_launcher) {
            final Tab tab = Tabs.getInstance().getSelectedTab();
            if (tab == null) {
                return true;
            }

            final String url = tab.getURL();
            final String title = tab.getDisplayTitle();
            if (url == null || title == null) {
                return true;
            }

            ThreadUtils.postToBackgroundThread(new Runnable() {
                @Override
                public void run() {
                    GeckoApplication.createBrowserShortcut(title, url);
                }
            });

            Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU,
                getResources().getResourceEntryName(itemId));
            return true;
        }

        if (itemId == R.id.set_as_homepage) {
            final Tab tab = Tabs.getInstance().getSelectedTab();
            if (tab == null) {
                return true;
            }

            final String url = tab.getURL();
            if (url == null) {
                return true;
            }
            final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
            final SharedPreferences.Editor editor = prefs.edit();
            editor.putString(GeckoPreferences.PREFS_HOMEPAGE, url);
            editor.apply();

            Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU,
                getResources().getResourceEntryName(itemId));
            return true;
        }

        return false;
    }

    @Override
    public void onDestroy() {
        if (mIsAbortingAppLaunch) {
            super.onDestroy();
            return;
        }

        if (mProgressView != null) {
            mProgressView.setDynamicToolbar(null);
        }

        mDynamicToolbar.destroy();

        final GeckoApplication app = (GeckoApplication) getApplication();
        app.getLightweightTheme().removeListener(this);

        if (mBrowserToolbar != null)
            mBrowserToolbar.onDestroy();

        if (mFindInPageBar != null) {
            mFindInPageBar.onDestroy();
            mFindInPageBar = null;
        }

        if (mMediaCastingBar != null) {
            mMediaCastingBar.onDestroy();
            mMediaCastingBar = null;
        }

        if (mSharedPreferencesHelper != null) {
            mSharedPreferencesHelper.uninit();
            mSharedPreferencesHelper = null;
        }

        if (mReadingListHelper != null) {
            mReadingListHelper.uninit();
            mReadingListHelper = null;
        }

        if (mAccountsHelper != null) {
            mAccountsHelper.uninit();
            mAccountsHelper = null;
        }

        if (mExtensionPermissionsHelper != null) {
            mExtensionPermissionsHelper.uninit();
            mExtensionPermissionsHelper = null;
        }

        mSearchEngineManager.unregisterListeners();

        EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
            "Search:Keyword",
            null);

        EventDispatcher.getInstance().unregisterUiThreadListener(this,
            "Accessibility:Enabled",
            "Menu:Open",
            "Menu:Update",
            "Menu:Add",
            "Menu:Remove",
            "Menu:AddBrowserAction",
            "Menu:RemoveBrowserAction",
            "Menu:UpdateBrowserAction",
            "LightweightTheme:Update",
            "Tab:Added",
            "Video:Play",
            "CharEncoding:Data",
            "CharEncoding:State",
            "Settings:Show",
            "Updater:Launch",
            "Sanitize:Finished",
            "Sanitize:OpenTabs",
            null);

        EventDispatcher.getInstance().unregisterBackgroundThreadListener(this,
            "Experiments:GetActive",
            "Experiments:SetOverride",
            "Experiments:ClearOverride",
            "Favicon:Request",
            "Feedback:MaybeLater",
            "Sanitize:ClearHistory",
            "Sanitize:ClearSyncedTabs",
            "Telemetry:Gather",
            "Download:AndroidDownloadManager",
            "Website:AppInstalled",
            "Website:AppInstallFailed",
            "Website:Metadata",
            null);

        getAppEventDispatcher().unregisterUiThreadListener(this, "Prompt:ShowTop");

        if (AppConstants.MOZ_ANDROID_BEAM) {
            NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
            if (nfc != null) {
                // null this out even though the docs say it's not needed,
                // because the source code looks like it will only do this
                // automatically on API 14+
                nfc.setNdefPushMessageCallback(null, this);
            }
        }

        for (final BrowserAppDelegate delegate : delegates) {
            delegate.onDestroy(this);
        }

        deleteTempFiles(getApplicationContext());

        NotificationHelper.destroy();
        GeckoNetworkManager.destroy();

        super.onDestroy();
    }

    @Override
    protected void initializeChrome() {
        super.initializeChrome();

        mDoorHangerPopup.setAnchor(mBrowserToolbar.getDoorHangerAnchor());
        mDoorHangerPopup.setOnVisibilityChangeListener(this);

        if (mLayerView != null) {
            mLayerView.getDynamicToolbarAnimator().setToolbarChromeProxy(this);
        }
        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();
        }
    }

    @Override
    public void onDoorHangerShow() {
        mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
        super.onDoorHangerShow();
    }

    // ToolbarChromeProxy inteface
    @Override
    public Bitmap getBitmapOfToolbarChrome() {
        if (mBrowserChrome == null) {
            return null;
        }

        Bitmap bm = Bitmap.createBitmap(mBrowserChrome.getWidth(), mBrowserChrome.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bm);
        Drawable bgDrawable = mBrowserChrome.getBackground();
        if (bgDrawable != null) {
            bgDrawable.draw(canvas);
        } else {
            canvas.drawColor(Color.WHITE);
        }

        mBrowserChrome.draw(canvas);
        return bm;
    }

    @Override
    public boolean isToolbarChromeVisible() {
       return mBrowserChrome.getVisibility() == View.VISIBLE;
    }

    @Override
    public void toggleToolbarChrome(final boolean aShow) {
        if (aShow) {
            mBrowserChrome.setVisibility(View.VISIBLE);
        } else {
            // The chrome needs to be INVISIBLE instead of GONE so that
            // it will continue update when the layout changes. This
            // ensures the bitmap generated for the static toolbar
            // snapshot is the correct size.
            mBrowserChrome.setVisibility(View.INVISIBLE);
        }
    }

    public void refreshToolbarHeight() {
        ThreadUtils.assertOnUiThread();

        int height = 0;
        if (mBrowserChrome != null) {
            height = mBrowserChrome.getHeight();
        }

        mHomeScreenContainer.setPadding(0, height, 0, 0);

        if (mLayerView != null && height != mToolbarHeight) {
            mToolbarHeight = height;
            mLayerView.getDynamicToolbarAnimator().setMaxToolbarHeight(height);
            mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
        }
    }

    @Override
    void toggleChrome(final boolean aShow) {
        if (mDynamicToolbar != null) {
            mDynamicToolbar.setVisible(aShow, VisibilityTransition.IMMEDIATE);
        }
        super.toggleChrome(aShow);
    }

    @Override
    void focusChrome() {
        if (mDynamicToolbar != null) {
            mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
        }
        mActionBarFlipper.requestFocusFromTouch();

        super.focusChrome();
    }

    @Override
    public void refreshChrome() {
        invalidateOptionsMenu();

        if (mTabsPanel != null) {
            mTabsPanel.refresh();
        }

        if (mTabStrip != null) {
            mTabStrip.refresh();
        }

        mBrowserToolbar.refresh();
    }

    @Override // BundleEventListener
    public void handleMessage(final String event, final GeckoBundle message,
                              final EventCallback callback) {
        switch (event) {
            case "Gecko:Ready":
                EventDispatcher.getInstance().registerUiThreadListener(this, "Gecko:DelayedStartup");

                // Handle this message in GeckoApp, but also enable the Settings
                // menuitem, which is specific to BrowserApp.
                super.handleMessage(event, message, callback);

                final Menu menu = mMenu;
                ThreadUtils.postToUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (menu != null) {
                            menu.findItem(R.id.settings).setEnabled(true);
                            menu.findItem(R.id.help).setEnabled(true);
                        }
                    }
                });

                // Display notification for Mozilla data reporting, if data should be collected.
                if (AppConstants.MOZ_DATA_REPORTING &&
                        Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) {
                    DataReportingNotification.checkAndNotifyPolicy(this);
                }
                break;

            case "Gecko:DelayedStartup":
                EventDispatcher.getInstance().unregisterUiThreadListener(this, "Gecko:DelayedStartup");

                // Force tabs panel inflation once the initial pageload is finished.
                ensureTabsPanelExists();

                if (AppConstants.MOZ_MEDIA_PLAYER) {
                    // Check if the fragment is already added. This should never be true
                    // here, but this is a nice safety check. If casting is disabled,
                    // these classes aren't built. We use reflection to initialize them.
                    final Class<?> mediaManagerClass = getMediaPlayerManager();

                    if (mediaManagerClass != null) {
                        try {
                            final String tag = "";
                            mediaManagerClass.getDeclaredField("MEDIA_PLAYER_TAG").get(tag);
                            Log.i(LOGTAG, "Found tag " + tag);
                            final Fragment frag = getSupportFragmentManager().findFragmentByTag(tag);
                            if (frag == null) {
                                final Method getInstance = mediaManagerClass.getMethod(
                                        "getInstance", (Class[]) null);
                                final Fragment mpm = (Fragment) getInstance.invoke(null);
                                getSupportFragmentManager().beginTransaction()
                                        .disallowAddToBackStack().add(mpm, tag).commit();
                            }
                        } catch (Exception ex) {
                            Log.e(LOGTAG, "Error initializing media manager", ex);
                        }
                    }
                }

                if (AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED &&
                        Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) {
                    // Start (this acts as ping if started already) the stumbler lib; if
                    // the stumbler has queued data it will upload it.  Stumbler operates
                    // on its own thread, and startup impact is further minimized by
                    // delaying work (such as upload) a few seconds.  Avoid any potential
                    // startup CPU/thread contention by delaying the pref broadcast.
                    GeckoPreferences.broadcastStumblerPref(BrowserApp.this);
                }

                if (AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE &&
                        !IntentUtils.getIsInAutomationFromEnvironment(new SafeIntent(getIntent()))) {
                    // TODO: Better scheduling of DLC actions (Bug 1257492)
                    DownloadContentService.startSync(this);
                    DownloadContentService.startVerification(this);
                }

                break;

            case "Accessibility:Enabled":
                mDynamicToolbar.setAccessibilityEnabled(message.getBoolean("enabled"));
                break;

            case "Menu:Open":
                if (mBrowserToolbar.isEditing()) {
                    mBrowserToolbar.cancelEdit();
                }
                openOptionsMenu();
                break;

            case "Menu:Update":
                updateAddonMenuItem(message.getInt("id") + ADDON_MENU_OFFSET,
                                    message.getBundle("options"));
                break;

            case "Menu:Add":
                final MenuItemInfo info = new MenuItemInfo();
                info.label = message.getString("name");
                if (info.label == null) {
                    Log.e(LOGTAG, "Invalid menu item name");
                    return;
                }
                info.id = message.getInt("id") + ADDON_MENU_OFFSET;
                info.checked = message.getBoolean("checked", false);
                info.enabled = message.getBoolean("enabled", true);
                info.visible = message.getBoolean("visible", true);
                info.checkable = message.getBoolean("checkable", false);
                final int parent = message.getInt("parent", 0);
                info.parent = parent <= 0 ? parent : parent + ADDON_MENU_OFFSET;
                addAddonMenuItem(info);
                break;

            case "Menu:Remove":
                removeAddonMenuItem(message.getInt("id") + ADDON_MENU_OFFSET);
                break;

            case "Menu:AddBrowserAction":
                final BrowserActionItemInfo browserAction = new BrowserActionItemInfo();
                browserAction.label = message.getString("name");
                if (TextUtils.isEmpty(browserAction.label)) {
                    Log.e(LOGTAG, "Invalid browser action name");
                    return;
                }
                browserAction.id = message.getInt("id") + BROWSER_ACTION_MENU_OFFSET;
                browserAction.uuid = message.getString("uuid");
                addBrowserActionMenuItem(browserAction);
                break;

            case "Menu:RemoveBrowserAction":
                removeBrowserActionMenuItem(message.getString("uuid"));
                break;

            case "Menu:UpdateBrowserAction":
                updateBrowserActionMenuItem(message.getString("uuid"),
                                            message.getBundle("options"));
                break;

            case "LightweightTheme:Update":
                mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
                break;

            case "Search:Keyword":
                storeSearchQuery(message.getString("query"));
                recordSearch(GeckoSharedPrefs.forProfile(this), message.getString("identifier"),
                        TelemetryContract.Method.ACTIONBAR);
                break;

            case "Prompt:ShowTop":
                // Bring this activity to front so the prompt is visible..
                Intent bringToFrontIntent = new Intent();
                bringToFrontIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
                                                AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
                bringToFrontIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
                startActivity(bringToFrontIntent);
                break;

            case "Tab:Added":
                if (message.getBoolean("cancelEditMode")) {
                    // Set the target tab to null so it does not get selected (on editing
                    // mode exit) in lieu of the tab that we're going to open and select.
                    mTargetTabForEditingMode = null;
                    mBrowserToolbar.cancelEdit();
                }
                break;

            case "Video:Play":
                if (SwitchBoard.isInExperiment(this, Experiments.HLS_VIDEO_PLAYBACK)) {
                    mVideoPlayer.start(Uri.parse(message.getString("uri")));
                    Telemetry.sendUIEvent(TelemetryContract.Event.SHOW,
                                          TelemetryContract.Method.CONTENT, "playhls");
                }
                break;

            case "CharEncoding:Data":
                final GeckoBundle[] charsets = message.getBundleArray("charsets");
                final int selected = message.getInt("selected");

                final String[] titleArray = new String[charsets.length];
                final String[] codeArray = new String[charsets.length];
                for (int i = 0; i < charsets.length; i++) {
                    final GeckoBundle charset = charsets[i];
                    titleArray[i] = charset.getString("title");
                    codeArray[i] = charset.getString("code");
                }

                final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
                dialogBuilder.setSingleChoiceItems(titleArray, selected,
                        new AlertDialog.OnClickListener() {
                            @Override
                            public void onClick(final DialogInterface dialog, final int which) {
                                final GeckoBundle data = new GeckoBundle(1);
                                data.putString("encoding", codeArray[which]);
                                EventDispatcher.getInstance().dispatch("CharEncoding:Set", data);
                                dialog.dismiss();
                            }
                        });
                dialogBuilder.setNegativeButton(R.string.button_cancel,
                        new AlertDialog.OnClickListener() {
                            @Override
                            public void onClick(final DialogInterface dialog, final int which) {
                                dialog.dismiss();
                            }
                        });
                dialogBuilder.show();
                break;

            case "CharEncoding:State":
                final boolean visible = "true".equals(message.getString("visible"));
                GeckoPreferences.setCharEncodingState(visible);
                if (mMenu != null) {
                    mMenu.findItem(R.id.char_encoding).setVisible(visible);
                }
                break;

            case "Experiments:GetActive":
                final List<String> experiments = SwitchBoard.getActiveExperiments(this);
                callback.sendSuccess(experiments.toArray(new String[experiments.size()]));
                break;

            case "Experiments:SetOverride":
                Experiments.setOverride(this, message.getString("name"),
                                        message.getBoolean("isEnabled"));
                break;

            case "Experiments:ClearOverride":
                Experiments.clearOverride(this, message.getString("name"));
                break;

            case "Favicon:Request":
                final String url = message.getString("url");
                final boolean shouldSkipNetwork = message.getBoolean("skipNetwork");

                if (TextUtils.isEmpty(url)) {
                    callback.sendError(null);
                    break;
                }

                Icons.with(this)
                        .pageUrl(url)
                        .privileged(false)
                        .skipNetworkIf(shouldSkipNetwork)
                        .executeCallbackOnBackgroundThread()
                        .build()
                        .execute(IconsHelper.createBase64EventCallback(callback));
                break;

            case "Feedback:MaybeLater":
                SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
                settings.edit().putInt(getPackageName() + ".feedback_launch_count", 0).apply();
                break;

            case "Sanitize:Finished":
                if (message.getBoolean("shutdown", false)) {
                    // Gecko is shutting down and has called our sanitize handlers,
                    // so we can start exiting, too.
                    finishAndShutdown(/* restart */ false);
                }
                break;

            case "Sanitize:OpenTabs":
                Tabs.getInstance().closeAllTabs();
                callback.sendSuccess(null);
                break;

            case "Sanitize:ClearHistory":
                BrowserDB.from(getProfile()).clearHistory(
                        getContentResolver(), message.getBoolean("clearSearchHistory", false));
                callback.sendSuccess(null);
                break;

            case "Sanitize:ClearSyncedTabs":
                FennecTabsRepository.deleteNonLocalClientsAndTabs(this);
                callback.sendSuccess(null);
                break;

            case "Settings:Show":
                final Intent settingsIntent = new Intent(this, GeckoPreferences.class);
                final String resource = message.getString(GeckoPreferences.INTENT_EXTRA_RESOURCES);

                GeckoPreferences.setResourceToOpen(settingsIntent, resource);
                startActivityForResult(settingsIntent, ACTIVITY_REQUEST_PREFERENCES);

                // Don't use a transition to settings if we're on a device where that
                // would look bad.
                if (HardwareUtils.IS_KINDLE_DEVICE) {
                    overridePendingTransition(0, 0);
                }
                break;

            case "Telemetry:Gather":
                final BrowserDB db = BrowserDB.from(getProfile());
                final ContentResolver cr = getContentResolver();

                Telemetry.addToHistogram("PLACES_PAGES_COUNT", db.getCount(cr, "history"));
                Telemetry.addToHistogram("FENNEC_BOOKMARKS_COUNT", db.getCount(cr, "bookmarks"));
                Telemetry.addToHistogram("BROWSER_IS_USER_DEFAULT",
                        (isDefaultBrowser(Intent.ACTION_VIEW) ? 1 : 0));
                Telemetry.addToHistogram("FENNEC_CUSTOM_HOMEPAGE",
                        (Tabs.hasHomepage(this) ? 1 : 0));

                final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
                final boolean hasCustomHomepanels =
                        prefs.contains(HomeConfigPrefsBackend.PREFS_CONFIG_KEY) ||
                        prefs.contains(HomeConfigPrefsBackend.PREFS_CONFIG_KEY_OLD);

                Telemetry.addToHistogram("FENNEC_HOMEPANELS_CUSTOM", hasCustomHomepanels ? 1 : 0);

                Telemetry.addToHistogram("FENNEC_READER_VIEW_CACHE_SIZE",
                        SavedReaderViewHelper.getSavedReaderViewHelper(this)
                                             .getDiskSpacedUsedKB());

                if (Versions.feature16Plus) {
                    Telemetry.addToHistogram("BROWSER_IS_ASSIST_DEFAULT",
                            (isDefaultBrowser(Intent.ACTION_ASSIST) ? 1 : 0));
                }

                Telemetry.addToHistogram("FENNEC_ORBOT_INSTALLED",
                    ContextUtils.isPackageInstalled(this, "org.torproject.android") ? 1 : 0);
                break;

            case "Website:AppInstalled":
                final String name = message.getString("name");
                final String startUrl = message.getString("start_url");
                final String manifestPath = message.getString("manifest_path");
                final String manifestUrl = message.getString("manifest_url");
                final LoadFaviconResult loadIconResult = FaviconDecoder
                    .decodeDataURI(this, message.getString("icon"));
                if (loadIconResult != null) {
                    final Bitmap icon = loadIconResult
                        .getBestBitmap(GeckoAppShell.getPreferredIconSize());
                    GeckoApplication.createAppShortcut(name, startUrl, manifestPath, manifestUrl, icon);

                    Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.PAGEACTION, PwaConfirm.TELEMETRY_EXTRA_ADDED);

                } else {
                    Log.e(LOGTAG, "Failed to load icon!");
                }

                break;

            case "Website:AppInstallFailed":
                final String title = message.getString("title");
                final String bookmarkUrl = message.getString("url");
                GeckoApplication.createBrowserShortcut(title, bookmarkUrl);
                break;

            case "Updater:Launch":
                /**
                 * Launch UI that lets the user update Firefox.
                 *
                 * This depends on the current channel: Release and Beta both direct to
                 * the Google Play Store. If updating is enabled, Aurora, Nightly, and
                 * custom builds open about:firefox, which provides an update interface.
                 *
                 * If updating is not enabled, this simply logs an error.
                 */
                if (AppConstants.RELEASE_OR_BETA) {
                    Intent intent = new Intent(Intent.ACTION_VIEW);
                    intent.setData(Uri.parse("market://details?id=" + getPackageName()));
                    startActivity(intent);
                    break;
                }

                if (AppConstants.MOZ_UPDATER) {
                    Tabs.getInstance().loadUrlInTab(AboutPages.FIREFOX);
                    break;
                }

                Log.w(LOGTAG, "No candidate updater found; ignoring launch request.");
                break;

            case "Download:AndroidDownloadManager":
                // Downloading via Android's download manager
                final String uri = message.getString("uri");
                final String filename = message.getString("filename");
                final String mimeType = message.getString("mimeType");

                final DownloadManager.Request request = new DownloadManager.Request(Uri.parse(uri));
                request.setMimeType(mimeType);

                try {
                    request.setDestinationInExternalPublicDir(
                            Environment.DIRECTORY_DOWNLOADS, filename);
                } catch (IllegalStateException e) {
                    Log.e(LOGTAG, "Cannot create download directory");
                    break;
                }

                request.allowScanningByMediaScanner();
                request.setNotificationVisibility(
                        DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
                request.addRequestHeader("User-Agent", HardwareUtils.isTablet() ?
                        AppConstants.USER_AGENT_FENNEC_TABLET :
                        AppConstants.USER_AGENT_FENNEC_MOBILE);

                try {
                    DownloadManager manager = (DownloadManager)
                            getSystemService(Context.DOWNLOAD_SERVICE);
                    manager.enqueue(request);
                } catch (RuntimeException e) {
                    Log.e(LOGTAG, "Download failed: " + e);
                }
                break;

            case "Website:Metadata":
                final String location = message.getString("location");
                final boolean hasImage = message.getBoolean("hasImage");
                final String metadata = message.getString("metadata");

                final ContentProviderClient contentProviderClient = getContentResolver()
                        .acquireContentProviderClient(BrowserContract.PageMetadata.CONTENT_URI);
                if (contentProviderClient == null) {
                    Log.w(LOGTAG, "Failed to obtain content provider client for: " +
                                  BrowserContract.PageMetadata.CONTENT_URI);
                    return;
                }
                try {
                    GlobalPageMetadata.getInstance().add(
                            BrowserDB.from(getProfile()),
                            contentProviderClient,
                            location, hasImage, metadata);
                } finally {
                    contentProviderClient.release();
                }

                break;

            default:
                super.handleMessage(event, message, callback);
                break;
        }
    }

    /**
     * Use a dummy Intent to do a default browser check.
     *
     * @return true if this package is the default browser on this device, false otherwise.
     */
    private boolean isDefaultBrowser(String action) {
        final Intent viewIntent = new Intent(action, Uri.parse("http://www.mozilla.org"));
        final ResolveInfo info = getPackageManager().resolveActivity(viewIntent, PackageManager.MATCH_DEFAULT_ONLY);
        if (info == null) {
            // No default is set
            return false;
        }

        final String packageName = info.activityInfo.packageName;
        return (TextUtils.equals(packageName, getPackageName()));
    }

    @Override
    public void addTab() {
        MmaDelegate.track(NEW_TAB);
        Tabs.getInstance().addTab();
    }

    @Override
    public void addPrivateTab() {
        Tabs.getInstance().addPrivateTab();
    }

    public void showTrackingProtectionPromptIfApplicable() {
        final SharedPreferences prefs = getSharedPreferences();

        final boolean hasTrackingProtectionPromptBeShownBefore = prefs.getBoolean(GeckoPreferences.PREFS_TRACKING_PROTECTION_PROMPT_SHOWN, false);

        if (hasTrackingProtectionPromptBeShownBefore) {
            return;
        }

        prefs.edit().putBoolean(GeckoPreferences.PREFS_TRACKING_PROTECTION_PROMPT_SHOWN, true).apply();

        startActivity(new Intent(BrowserApp.this, TrackingProtectionPrompt.class));
    }

    @Override
    public void showNormalTabs() {
        showTabs(TabsPanel.Panel.NORMAL_TABS);
    }

    @Override
    public void showPrivateTabs() {
        showTabs(TabsPanel.Panel.PRIVATE_TABS);
    }
    /**
    * Ensure the TabsPanel view is properly inflated and returns
    * true when the view has been inflated, false otherwise.
    */
    private boolean ensureTabsPanelExists() {
        if (mTabsPanel != null) {
            return false;
        }

        ViewStub tabsPanelStub = (ViewStub) findViewById(R.id.tabs_panel);
        mTabsPanel = (TabsPanel) tabsPanelStub.inflate();

        mTabsPanel.setTabsLayoutChangeListener(this);

        return true;
    }

    private void showTabs(final TabsPanel.Panel panel) {
        if (Tabs.getInstance().getDisplayCount() == 0)
            return;

        hideFirstrunPager(TelemetryContract.Method.BUTTON);

        if (ensureTabsPanelExists()) {
            // If we've just inflated the tabs panel, only show it once the current
            // layout pass is done to avoid displayed temporary UI states during
            // relayout.
            ViewTreeObserver vto = mTabsPanel.getViewTreeObserver();
            if (vto.isAlive()) {
                vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        mTabsPanel.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                        showTabs(panel);
                    }
                });
            }
        } else {
            if (mDoorHangerPopup != null) {
                mDoorHangerPopup.disable();
            }
            if (mTabStrip != null) {
                mTabStrip.tabStripIsCovered(true);
            }
            mTabsPanel.show(panel);

            // Hide potentially visible "find in page" bar (Bug 1177338)
            mFindInPageBar.hide();

            for (final BrowserAppDelegate delegate : delegates) {
                delegate.onTabsTrayShown(this, mTabsPanel);
            }
        }

        // Set status bar color with tabs tray background color.
        WindowUtil.setTabsTrayStatusBarColor(this);
    }

    @Override
    public void hideTabs() {
        mTabsPanel.hide();
        if (mTabStrip != null) {
            mTabStrip.tabStripIsCovered(false);
        }
        if (mDoorHangerPopup != null) {
            mDoorHangerPopup.enable();
        }

        for (final BrowserAppDelegate delegate : delegates) {
            delegate.onTabsTrayHidden(this, mTabsPanel);
        }

        refreshStatusBarColor();
    }

    @Override
    public boolean autoHideTabs() {
        if (areTabsShown()) {
            hideTabs();
            return true;
        }
        return false;
    }

    public boolean areTabsShown() {
        return (mTabsPanel != null && mTabsPanel.isShown());
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    @Override
    public void onTabsLayoutChange(int width, int height) {
        int animationLength = TABS_ANIMATION_DURATION;

        if (mMainLayoutAnimator != null) {
            animationLength = Math.max(1, animationLength - (int)mMainLayoutAnimator.getRemainingTime());
            mMainLayoutAnimator.stop(false);
        }

        if (areTabsShown()) {
            mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            // Hide the web content from accessibility tools even though it's visible
            // so that you can't examine it as long as the tabs are being shown.
            if (Versions.feature16Plus) {
                mLayerView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
            }
        } else {
            if (Versions.feature16Plus) {
                mLayerView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
            }
        }

        mMainLayoutAnimator = new PropertyAnimator(animationLength, sTabsInterpolator);
        mMainLayoutAnimator.addPropertyAnimationListener(this);
        mMainLayoutAnimator.attach(mMainLayout,
                                   PropertyAnimator.Property.SCROLL_Y,
                                   -height);

        mTabsPanel.prepareTabsAnimation(mMainLayoutAnimator);
        mBrowserToolbar.triggerTabsPanelTransition(mMainLayoutAnimator, areTabsShown());

        // If the tabs panel is animating onto the screen, pin the dynamic
        // toolbar.
        if (mDynamicToolbar.isEnabled()) {
            if (width > 0 && height > 0) {
                mDynamicToolbar.setPinned(true, PinReason.RELAYOUT);
                mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
            } else {
                mDynamicToolbar.setPinned(false, PinReason.RELAYOUT);
            }
        }

        mMainLayoutAnimator.start();
    }

    @Override
    public void onPropertyAnimationStart() {
    }

    @Override
    public void onPropertyAnimationEnd() {
        if (!areTabsShown()) {
            mTabsPanel.setVisibility(View.INVISIBLE);
            mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
        } else {
            // Cancel editing mode to return to page content when the TabsPanel closes. We cancel
            // it here because there are graphical glitches if it's canceled while it's visible.
            mBrowserToolbar.cancelEdit();
        }

        mTabsPanel.finishTabsAnimation();

        mMainLayoutAnimator = null;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        mDynamicToolbar.onSaveInstanceState(outState);
        outState.putInt(STATE_ABOUT_HOME_TOP_PADDING, mHomeScreenContainer.getPaddingTop());

        // The various add-on UI item caches and event listeners should really live somewhere based
        // on the Application, so that their lifetime more closely matches that of Gecko itself, as
        // GeckoView-based activities can start Gecko (and therefore add-ons) while BrowserApp isn't
        // even running.
        // For now we'll only guard against the case where BrowserApp is destroyed and later re-
        // created while Gecko keeps running throughout, and leave the full solution to bug 1414084.
        outState.putParcelableArrayList(STATE_ADDON_MENU_ITEM_CACHE, mAddonMenuItemsCache);
        outState.putParcelableArrayList(STATE_BROWSER_ACTION_ITEM_CACHE, mBrowserActionItemsCache);
    }

    /**
     * Attempts to switch to an open tab with the given URL.
     * <p>
     * If the tab exists, this method cancels any in-progress editing as well as
     * calling {@link Tabs#selectTab(int)}.
     *
     * @param url of tab to switch to.
     * @param flags to obey: if {@link OnUrlOpenListener.Flags#ALLOW_SWITCH_TO_TAB}
     *        is not present, return false.
     * @return true if we successfully switched to a tab, false otherwise.
     */
    private boolean maybeSwitchToTab(String url, EnumSet<OnUrlOpenListener.Flags> flags) {
        if (!flags.contains(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)) {
            return false;
        }

        final boolean isPrivate = mBrowserToolbar.isPrivateMode();
        final Tabs tabs = Tabs.getInstance();
        final Tab tab;

        if (AboutPages.isAboutReader(url)) {
            tab = tabs.getFirstReaderTabForUrl(url, isPrivate);
        } else {
            tab = tabs.getFirstTabForUrl(url, isPrivate);
        }

        if (tab == null) {
            return false;
        }

        return maybeSwitchToTab(tab.getId());
    }

    /**
     * Attempts to switch to an open tab with the given unique tab ID.
     * <p>
     * If the tab exists, this method cancels any in-progress editing as well as
     * calling {@link Tabs#selectTab(int)}.
     *
     * @param id of tab to switch to.
     * @return true if we successfully switched to the tab, false otherwise.
     */
    private boolean maybeSwitchToTab(int id) {
        final Tabs tabs = Tabs.getInstance();
        final Tab tab = tabs.getTab(id);

        if (tab == null) {
            return false;
        }

        final Tab oldTab = tabs.getSelectedTab();
        if (oldTab != null) {
            oldTab.setIsEditing(false);
        }

        // Set the target tab to null so it does not get selected (on editing
        // mode exit) in lieu of the tab we are about to select.
        mTargetTabForEditingMode = null;
        tabs.selectTab(tab.getId());

        mBrowserToolbar.cancelEdit();

        return true;
    }

    public void openUrlAndStopEditing(String url) {
        openUrlAndStopEditing(url, null, null, false);
    }

    private void openUrlAndStopEditingWithReferrer(final String url, final String referrerUri) {
        openUrlAndStopEditing(url, null, referrerUri, false);
    }

    private void openUrlAndStopEditing(String url, String searchEngine) {
        openUrlAndStopEditing(url, searchEngine, null, false);
    }

    private void openUrlAndStopEditing(String url, String searchEngine, @Nullable final String referrerUri,
            boolean newTab) {
        int flags = Tabs.LOADURL_NONE;
        if (newTab) {
            flags |= Tabs.LOADURL_NEW_TAB;
            if (Tabs.getInstance().getSelectedTab().isPrivate()) {
                flags |= Tabs.LOADURL_PRIVATE;
            }
        }

        Tabs.getInstance().loadUrl(url, searchEngine, referrerUri, Tabs.INVALID_TAB_ID, null, flags);

        mBrowserToolbar.cancelEdit();
    }

    private boolean isHomePagerVisible() {
        return (mHomeScreen != null && mHomeScreen.isVisible()
                && mHomeScreenContainer != null && mHomeScreenContainer.getVisibility() == View.VISIBLE);
    }

    private boolean isFirstrunVisible() {
        return (mFirstrunAnimationContainer != null && mFirstrunAnimationContainer.isVisible()
                && mHomeScreenContainer != null && mHomeScreenContainer.getVisibility() == View.VISIBLE);
    }

    /**
     * Enters editing mode with the current tab's URL. There might be no
     * tabs loaded by the time the user enters editing mode e.g. just after
     * the app starts. In this case, we simply fallback to an empty URL.
     */
    private void enterEditingMode() {
        String url = "";
        String telemetryMsg = "urlbar-empty";

        final Tab tab = Tabs.getInstance().getSelectedTab();
        if (tab != null) {
            final String userSearchTerm = tab.getUserRequested();
            final String tabURL = tab.getURL();

            // Check to see if there's a user-entered search term,
            // which we save whenever the user performs a search.
            if (!TextUtils.isEmpty(userSearchTerm)) {
                url = userSearchTerm;
                telemetryMsg = "urlbar-userentered";
            } else if (!TextUtils.isEmpty(tabURL)) {
                url = tabURL;
                telemetryMsg = "urlbar-url";
                if (splashScreen != null) {
                    splashScreen.setVisibility(View.GONE);
                }
            }
        }

        enterEditingMode(url);
        Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.ACTIONBAR, telemetryMsg);
    }

    /**
     * Enters editing mode with the specified URL. If a null
     * url is given, the empty String will be used instead.
     */
    private void enterEditingMode(@NonNull String url) {
        hideFirstrunPager(TelemetryContract.Method.ACTIONBAR);

        if (mBrowserToolbar.isEditing() || mBrowserToolbar.isAnimating()) {
            return;
        }

        final Tab selectedTab = Tabs.getInstance().getSelectedTab();
        final String panelId;
        if (selectedTab != null) {
            mTargetTabForEditingMode = selectedTab.getId();
            panelId = selectedTab.getMostRecentHomePanel();
        } else {
            mTargetTabForEditingMode = null;
            panelId = null;
        }

        final PropertyAnimator animator = new PropertyAnimator(250);
        animator.setUseHardwareLayer(false);

        mBrowserToolbar.startEditing(url, animator);

        showHomePagerWithAnimator(panelId, null, animator);

        animator.start();
        Telemetry.startUISession(TelemetryContract.Session.AWESOMESCREEN);
    }

    private void commitEditingMode() {
        if (!mBrowserToolbar.isEditing()) {
            return;
        }

        Telemetry.stopUISession(TelemetryContract.Session.AWESOMESCREEN,
                                TelemetryContract.Reason.COMMIT);

        final String url = mBrowserToolbar.commitEdit();

        // HACK: We don't know the url that will be loaded when hideHomePager is initially called
        // in BrowserToolbar's onStopEditing listener so on the awesomescreen, hideHomePager will
        // use the url "about:home" and return without taking any action. hideBrowserSearch is
        // then called, but since hideHomePager changes both HomePager and LayerView visibility
        // and exited without taking an action, no Views are displayed and graphical corruption is
        // visible instead.
        //
        // Here we call hideHomePager for the second time with the URL to be loaded so that
        // hideHomePager is called with the correct state for the upcoming page load.
        //
        // Expected to be fixed by bug 915825.
        hideHomePager(url);
        loadUrlOrKeywordSearch(url);
        clearSelectedTabApplicationId();
    }

    private void clearSelectedTabApplicationId() {
        final Tab selected = Tabs.getInstance().getSelectedTab();
        if (selected != null) {
            selected.setApplicationId(null);
        }
    }

    private void loadUrlOrKeywordSearch(final String url) {
        // Don't do anything if the user entered an empty URL.
        if (TextUtils.isEmpty(url)) {
            return;
        }

        // If the URL doesn't look like a search query, just load it.
        if (!StringUtils.isSearchQuery(url, true)) {
            Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED);
            Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.ACTIONBAR, "user");
            return;
        }

        // Otherwise, check for a bookmark keyword.
        final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfile(this);
        final BrowserDB db = BrowserDB.from(getProfile());
        ThreadUtils.postToBackgroundThread(new Runnable() {
            @Override
            public void run() {
                final String keyword;
                final String keywordSearch;

                final int index = url.indexOf(" ");
                if (index == -1) {
                    keyword = url;
                    keywordSearch = "";
                } else {
                    keyword = url.substring(0, index);
                    keywordSearch = url.substring(index + 1);
                }

                final String keywordUrl = db.getUrlForKeyword(getContentResolver(), keyword);

                // If there isn't a bookmark keyword, load the url. This may result in a query
                // using the default search engine.
                if (TextUtils.isEmpty(keywordUrl)) {
                    Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED);
                    Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.ACTIONBAR, "user");
                    return;
                }

                // Otherwise, construct a search query from the bookmark keyword.
                // Replace lower case bookmark keywords with URLencoded search query or
                // replace upper case bookmark keywords with un-encoded search query.
                // This makes it match the same behaviour as on Firefox for the desktop.
                final String searchUrl = keywordUrl.replace("%s", URLEncoder.encode(keywordSearch)).replace("%S", keywordSearch);

                Tabs.getInstance().loadUrl(searchUrl, Tabs.LOADURL_USER_ENTERED);
                Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL,
                                      TelemetryContract.Method.ACTIONBAR,
                                      "keyword");
            }
        });
    }

    /**
     * Records in telemetry that a search has occurred.
     *
     * @param where where the search was started from
     */
    private static void recordSearch(@NonNull final SharedPreferences prefs, @NonNull final String engineIdentifier,
            @NonNull final TelemetryContract.Method where) {
        // We could include the engine identifier as an extra but we'll
        // just capture that with core ping telemetry (bug 1253319).
        Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH, where);
        SearchCountMeasurements.incrementSearch(prefs, engineIdentifier, where.toString());
    }

    /**
     * Store search query in SearchHistoryProvider.
     *
     * @param query
     *        a search query to store. We won't store empty queries.
     */
    private void storeSearchQuery(final String query) {
        if (TextUtils.isEmpty(query)) {
            return;
        }

        // Filter out URLs and long suggestions
        if (query.length() > 50 || Pattern.matches("^(https?|ftp|file)://.*", query)) {
            return;
        }

        final GeckoProfile profile = getProfile();
        // Don't bother storing search queries in guest mode
        if (profile.inGuestMode()) {
            return;
        }

        final BrowserDB db = BrowserDB.from(profile);
        ThreadUtils.postToBackgroundThread(new Runnable() {
            @Override
            public void run() {
                db.getSearches().insert(getContentResolver(), query);
            }
        });
    }

    void filterEditingMode(String searchTerm, AutocompleteHandler handler) {
        if (TextUtils.isEmpty(searchTerm)) {
            hideBrowserSearch();
        } else {
            showBrowserSearch();
            mBrowserSearch.filter(searchTerm, handler);
        }
    }

    /**
     * Selects the target tab for editing mode. This is expected to be the tab selected on editing
     * mode entry, unless it is subsequently overridden.
     *
     * A background tab may be selected while editing mode is active (e.g. popups), causing the
     * new url to load in the newly selected tab. Call this method on editing mode exit to
     * mitigate this.
     *
     * Note that this method is disabled for new tablets because we can see the selected tab in the
     * tab strip and, when the selected tab changes during editing mode as in this hack, the
     * temporarily selected tab is visible to users.
     */
    private void selectTargetTabForEditingMode() {
        if (HardwareUtils.isTablet()) {
            return;
        }

        if (mTargetTabForEditingMode != null) {
            Tabs.getInstance().selectTab(mTargetTabForEditingMode);
        }

        mTargetTabForEditingMode = null;
    }

    /**
     * Shows or hides the home pager for the given tab.
     */
    private void updateHomePagerForTab(Tab tab) {
        // Don't change the visibility of the home pager if we're in editing mode.
        if (mBrowserToolbar.isEditing()) {
            return;
        }

        // History will only store that we were visiting about:home, however the specific panel
        // isn't stored. (We are able to navigate directly to homepanels using an about:home?panel=...
        // URL, but the reverse doesn't apply: manually switching panels doesn't update the URL.)
        // Hence we need to restore the panel, in addition to panel state, here.
        if (isAboutHome(tab)) {
            // For some reason(e.g. from SearchWidget) we are showing the splash schreen. We should hide it now.
            if (splashScreen != null && splashScreen.getVisibility() == View.VISIBLE) {
                // Below line will be run when LOCATION_CHANGE. Which means the page load is almost completed.
                splashScreen.hide();
            }

            String panelId = AboutPages.getPanelIdFromAboutHomeUrl(tab.getURL());
            Bundle panelRestoreData = null;
            if (panelId == null) {
                // No panel was specified in the URL. Try loading the most recent
                // home panel for this tab.
                // Note: this isn't necessarily correct. We don't update the URL when we switch tabs.
                // If a user explicitly navigated to about:reader?panel=FOO, and then switches
                // to panel BAR, the history URL still contains FOO, and we restore to FOO. In most
                // cases however we aren't supplying a panel ID in the URL so this code still works
                // for most cases.
                // We can't fix this directly since we can't ignore the panelId if we're explicitly
                // loading a specific panel, and we currently can't distinguish between loading
                // history, and loading new pages, see Bug 1268887
                panelId = tab.getMostRecentHomePanel();
                panelRestoreData = tab.getMostRecentHomePanelData();
            } else if (panelId.equals(HomeConfig.getIdForBuiltinPanelType(PanelType.DEPRECATED_RECENT_TABS))) {
                // Redirect to the Combined History panel.
                panelId = HomeConfig.getIdForBuiltinPanelType(PanelType.COMBINED_HISTORY);
                panelRestoreData = new Bundle();
                // Jump directly to the Recent Tabs subview of the Combined History panel.
                panelRestoreData.putBoolean("goToRecentTabs", true);
            }
            showHomePager(panelId, panelRestoreData);

            if (mDynamicToolbar.isEnabled()) {
                mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
            }
            showSplashScreen = false;
        } else {
            // The tab going to load is not about page. It's a web page.
            // If showSplashScreen is true, it means the app is first launched. We want to show the SlashScreen
            // But if GeckoThread.isRunning, the will be 0 sec for web rendering.
            // In that case, we don't want to show the SlashScreen/
            if (showSplashScreen && !GeckoThread.isRunning()) {

                final ViewGroup main = (ViewGroup) findViewById(R.id.gecko_layout);
                final View splashLayout = LayoutInflater.from(this).inflate(R.layout.splash_screen, main);
                splashScreen = (SplashScreen) splashLayout.findViewById(R.id.splash_root);

                showSplashScreen = false;
            } else if (splashScreen != null) {
                // Below line will be run when LOCATION_CHANGE. Which means the page load is almost completed.
                splashScreen.hide();
            }
            hideHomePager();
        }
    }

    @Override
    public void onLocaleReady(final String locale) {
        Log.d(LOGTAG, "onLocaleReady: " + locale);
        super.onLocaleReady(locale);

        HomePanelsManager.getInstance().onLocaleReady(locale);

        if (mMenu != null) {
            mMenu.clear();
            onCreateOptionsMenu(mMenu);
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        Log.d(LOGTAG, "onActivityResult: " + requestCode + ", " + resultCode + ", " + data);
        switch (requestCode) {
            case ACTIVITY_REQUEST_PREFERENCES:
                // We just returned from preferences. If our locale changed,
                // we need to redisplay at this point, and do any other browser-level
                // bookkeeping that we associate with a locale change.
                if (resultCode != GeckoPreferences.RESULT_CODE_LOCALE_DID_CHANGE) {
                    Log.d(LOGTAG, "No locale change returning from preferences; nothing to do.");
                    return;
                }

                ThreadUtils.postToBackgroundThread(new Runnable() {
                    @Override
                    public void run() {
                        final LocaleManager localeManager = BrowserLocaleManager.getInstance();
                        final Locale locale = localeManager.getCurrentLocale(getApplicationContext());
                        Log.d(LOGTAG, "Read persisted locale " + locale);
                        if (locale == null) {
                            return;
                        }
                        onLocaleChanged(Locales.getLanguageTag(locale));
                    }
                });
                break;

            case ACTIVITY_REQUEST_TAB_QUEUE:
                TabQueueHelper.processTabQueuePromptResponse(resultCode, this);
                break;

            default:
                for (final BrowserAppDelegate delegate : delegates) {
                    delegate.onActivityResult(this, requestCode, resultCode, data);
                }

                super.onActivityResult(requestCode, resultCode, data);
        }
    }

    private void showFirstrunPager() {

        if (mFirstrunAnimationContainer == null) {
            final ViewStub firstrunPagerStub = (ViewStub) findViewById(R.id.firstrun_pager_stub);
            mFirstrunAnimationContainer = (FirstrunAnimationContainer) firstrunPagerStub.inflate();
            mFirstrunAnimationContainer.load(getApplicationContext(), getSupportFragmentManager());
            mFirstrunAnimationContainer.registerOnFinishListener(new FirstrunAnimationContainer.OnFinishListener() {
                @Override
                public void onFinish() {
                    if (mFirstrunAnimationContainer.showBrowserHint() &&
                        !Tabs.hasHomepage(BrowserApp.this)) {
                        enterEditingMode();
                    }
                }
            });
        }

        mHomeScreenContainer.setVisibility(View.VISIBLE);
    }

    private void showHomePager(String panelId, Bundle panelRestoreData) {
        showHomePagerWithAnimator(panelId, panelRestoreData, null);
    }

    private void showHomePagerWithAnimator(String panelId, Bundle panelRestoreData, PropertyAnimator animator) {
        if (isHomePagerVisible()) {
            // Home pager already visible, make sure it shows the correct panel.
            mHomeScreen.showPanel(panelId, panelRestoreData);
            return;
        }

        // This must be called before the dynamic toolbar is set visible because it calls
        // FormAssistPopup.onMetricsChanged, which queues a runnable that undoes the effect of hide.
        // With hide first, onMetricsChanged will return early instead.
        mFormAssistPopup.hide();
        mFindInPageBar.hide();

        // Refresh toolbar height to possibly restore the toolbar padding
        refreshToolbarHeight();

        // Show the toolbar before hiding about:home so the
        // onMetricsChanged callback still works.
        if (mDynamicToolbar.isEnabled()) {
            mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
        }

        if (mHomeScreen == null) {
            if (ActivityStream.isEnabled(this) &&
                !ActivityStream.isHomePanel()) {
                final ViewStub asStub = (ViewStub) findViewById(R.id.activity_stream_stub);
                mHomeScreen = (HomeScreen) asStub.inflate();
            } else {
                final ViewStub homePagerStub = (ViewStub) findViewById(R.id.home_pager_stub);
                mHomeScreen = (HomeScreen) homePagerStub.inflate();

                // For now these listeners are HomePager specific. In future we might want
                // to have a more abstracted data storage, with one Bundle containing all
                // relevant restore data.
                mHomeScreen.setOnPanelChangeListener(new HomeScreen.OnPanelChangeListener() {
                    @Override
                    public void onPanelSelected(String panelId) {
                        final Tab currentTab = Tabs.getInstance().getSelectedTab();
                        if (currentTab != null) {
                            currentTab.setMostRecentHomePanel(panelId);
                        }
                    }
                });

                // Set this listener to persist restore data (via the Tab) every time panel state changes.
                mHomeScreen.setPanelStateChangeListener(new HomeFragment.PanelStateChangeListener() {
                    @Override
                    public void onStateChanged(Bundle bundle) {
                        final Tab currentTab = Tabs.getInstance().getSelectedTab();
                        if (currentTab != null) {
                            currentTab.setMostRecentHomePanelData(bundle);
                        }
                    }

                    @Override
                    public void setCachedRecentTabsCount(int count) {
                        mCachedRecentTabsCount = count;
                    }

                    @Override
                    public int getCachedRecentTabsCount() {
                        return mCachedRecentTabsCount;
                    }
                });
            }

            // Don't show the banner in guest mode.
            if (!Restrictions.isUserRestricted()) {
                final ViewStub homeBannerStub = (ViewStub) findViewById(R.id.home_banner_stub);
                final HomeBanner homeBanner = (HomeBanner) homeBannerStub.inflate();
                mHomeScreen.setBanner(homeBanner);

                // Remove the banner from the view hierarchy if it is dismissed.
                homeBanner.setOnDismissListener(new HomeBanner.OnDismissListener() {
                    @Override
                    public void onDismiss() {
                        mHomeScreen.setBanner(null);
                        mHomeScreenContainer.removeView(homeBanner);
                    }
                });
            }
        }

        mHomeScreenContainer.setVisibility(View.VISIBLE);
        mHomeScreen.load(getSupportLoaderManager(),
                        getSupportFragmentManager(),
                        panelId,
                        panelRestoreData,
                        animator);

        // Hide the web content so it cannot be focused by screen readers.
        hideWebContentOnPropertyAnimationEnd(animator);
    }

    private void hideWebContentOnPropertyAnimationEnd(final PropertyAnimator animator) {
        if (animator == null) {
            hideWebContent();
            return;
        }

        animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
            @Override
            public void onPropertyAnimationStart() {
                mHideWebContentOnAnimationEnd = true;
            }

            @Override
            public void onPropertyAnimationEnd() {
                if (mHideWebContentOnAnimationEnd) {
                    hideWebContent();
                }
            }
        });
    }

    private void hideWebContent() {
        // The view is set to INVISIBLE, rather than GONE, to avoid
        // the additional requestLayout() call.
        mLayerView.setVisibility(View.INVISIBLE);
    }

    /**
     * Hide the Onboarding pager on user action, and don't show any onFinish hints.
     * @param method TelemetryContract method by which action was taken
     * @return boolean of whether pager was visible
     */
    private boolean hideFirstrunPager(TelemetryContract.Method method) {
        if (!isFirstrunVisible()) {
            return false;
        }

        Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, method, "firstrun-pane");

        // Don't show any onFinish actions when hiding from this Activity.
        mFirstrunAnimationContainer.registerOnFinishListener(null);
        mFirstrunAnimationContainer.hide();
        return true;
    }

    /**
     * Hides the HomePager, using the url of the currently selected tab as the url to be
     * loaded.
     */
    private void hideHomePager() {
        final Tab selectedTab = Tabs.getInstance().getSelectedTab();
        final String url = (selectedTab != null) ? selectedTab.getURL() : null;

        hideHomePager(url);
    }

    /**
     * Hides the HomePager. The given url should be the url of the page to be loaded, or null
     * if a new page is not being loaded.
     */
    private void hideHomePager(final String url) {
        if (!isHomePagerVisible() || AboutPages.isAboutHome(url)) {
            return;
        }

        // Prevent race in hiding web content - see declaration for more info.
        mHideWebContentOnAnimationEnd = false;

        // Display the previously hidden web content (which prevented screen reader access).
        mLayerView.setVisibility(View.VISIBLE);
        mHomeScreenContainer.setVisibility(View.GONE);

        if (mHomeScreen != null) {
            mHomeScreen.unload();
        }

        mBrowserToolbar.setNextFocusDownId(R.id.layer_view);

        // Refresh toolbar height to possibly restore the toolbar padding
        refreshToolbarHeight();
    }

    private void showBrowserSearchAfterAnimation(PropertyAnimator animator) {
        if (animator == null) {
            showBrowserSearch();
            return;
        }

        animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
            @Override
            public void onPropertyAnimationStart() {
            }

            @Override
            public void onPropertyAnimationEnd() {
                showBrowserSearch();
            }
        });
    }

    private void showBrowserSearch() {
        if (mBrowserSearch.getUserVisibleHint()) {
            return;
        }

        mBrowserSearchContainer.setVisibility(View.VISIBLE);

        // Prevent overdraw by hiding the underlying web content and HomePager View
        hideWebContent();
        mHomeScreenContainer.setVisibility(View.INVISIBLE);

        final FragmentManager fm = getSupportFragmentManager();

        // In certain situations, showBrowserSearch() can be called immediately after hideBrowserSearch()
        // (see bug 925012). Because of an Android bug (http://code.google.com/p/android/issues/detail?id=61179),
        // calling FragmentTransaction#add immediately after FragmentTransaction#remove won't add the fragment's
        // view to the layout. Calling FragmentManager#executePendingTransactions before re-adding the fragment
        // prevents this issue.
        fm.executePendingTransactions();

        Fragment f = fm.findFragmentById(R.id.search_container);

        // checking if fragment is already present
        if (f != null) {
            fm.beginTransaction().show(f).commitAllowingStateLoss();
            mBrowserSearch.resetScrollState();
        } else {
            // add fragment if not already present
            fm.beginTransaction().add(R.id.search_container, mBrowserSearch, BROWSER_SEARCH_TAG).commitAllowingStateLoss();
        }
        mBrowserSearch.setUserVisibleHint(true);

        // We want to adjust the window size when the keyboard appears to bring the
        // SearchEngineBar above the keyboard. However, adjusting the window size
        // when hiding the keyboard results in graphical glitches where the keyboard was
        // because nothing was being drawn underneath (bug 933422). This can be
        // prevented drawing content under the keyboard (i.e. in the Window).
        //
        // We do this here because there are glitches when unlocking a device with
        // BrowserSearch in the foreground if we use BrowserSearch.onStart/Stop.
        getWindow().setBackgroundDrawableResource(android.R.color.white);
    }

    private void hideBrowserSearch() {
        if (!mBrowserSearch.getUserVisibleHint()) {
            return;
        }

        final Tab selectedTab = Tabs.getInstance().getSelectedTab();
        final String panelId;
        final Bundle panelData;
        if (selectedTab != null) {
            panelId = selectedTab.getMostRecentHomePanel();
            panelData = selectedTab.getMostRecentHomePanelData();
        } else {
            panelId = null;
            panelData = null;
        }

        // To prevent overdraw, the HomePager is hidden when BrowserSearch is displayed:
        // reverse that.
        showHomePager(panelId, panelData);

        mBrowserSearchContainer.setVisibility(View.INVISIBLE);

        getSupportFragmentManager().beginTransaction()
                .hide(mBrowserSearch).commitAllowingStateLoss();
        mBrowserSearch.setUserVisibleHint(false);

        getWindow().setBackgroundDrawable(null);
    }

    /**
     * Hides certain UI elements (e.g. button toast) when the user touches the main layout.
     */
    private static final class HideOnTouchListener implements TouchEventInterceptor {
        @Override
        public boolean onInterceptTouchEvent(View view, MotionEvent event) {
            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
                SnackbarBuilder.dismissCurrentSnackbar();
            }
            return false;
        }

        @Override
        public boolean onTouch(View view, MotionEvent event) {
            return false;
        }
    }

    private static Menu findParentMenu(Menu menu, MenuItem item) {
        final int itemId = item.getItemId();

        final int count = (menu != null) ? menu.size() : 0;
        for (int i = 0; i < count; i++) {
            MenuItem menuItem = menu.getItem(i);
            if (menuItem.getItemId() == itemId) {
                return menu;
            }
            if (menuItem.hasSubMenu()) {
                Menu parent = findParentMenu(menuItem.getSubMenu(), item);
                if (parent != null) {
                    return parent;
                }
            }
        }

        return null;
    }

    /**
     * Add the provided item to the provided menu, which should be
     * the root (mMenu).
     */
    private void addAddonMenuItemToMenu(final Menu menu, final MenuItemInfo info) {
        info.added = true;

        final Menu destination;
        if (info.parent == 0) {
            destination = menu;
        } else if (info.parent == GECKO_TOOLS_MENU) {
            // The tools menu only exists in our -v11 resources.
            final MenuItem tools = menu.findItem(R.id.tools);
            destination = tools != null ? tools.getSubMenu() : menu;
        } else {
            final MenuItem parent = menu.findItem(info.parent);
            if (parent == null) {
                return;
            }

            Menu parentMenu = findParentMenu(menu, parent);

            if (!parent.hasSubMenu()) {
                parentMenu.removeItem(parent.getItemId());
                destination = parentMenu.addSubMenu(Menu.NONE, parent.getItemId(), Menu.NONE, parent.getTitle());
                if (parent.getIcon() != null) {
                    ((SubMenu) destination).getItem().setIcon(parent.getIcon());
                }
            } else {
                destination = parent.getSubMenu();
            }
        }

        final MenuItem item = destination.add(Menu.NONE, info.id, Menu.NONE, info.label);

        item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
            @Override
            public boolean onMenuItemClick(MenuItem item) {
                final GeckoBundle data = new GeckoBundle(1);
                data.putInt("item", info.id - ADDON_MENU_OFFSET);
                EventDispatcher.getInstance().dispatch("Menu:Clicked", data);
                return true;
            }
        });

        item.setCheckable(info.checkable);
        item.setChecked(info.checked);
        item.setEnabled(info.enabled);
        item.setVisible(info.visible);
    }

    private void addAddonMenuItem(final MenuItemInfo info) {
        if (mAddonMenuItemsCache == null) {
            mAddonMenuItemsCache = new ArrayList<MenuItemInfo>();
        }

        // Mark it as added if the menu was ready.
        info.added = (mMenu != null);

        // Always cache so we can rebuild after a locale switch.
        mAddonMenuItemsCache.add(info);

        if (mMenu == null) {
            return;
        }

        addAddonMenuItemToMenu(mMenu, info);
    }

    private void removeAddonMenuItem(int id) {
        // Remove add-on menu item from cache, if available.
        if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
            for (MenuItemInfo item : mAddonMenuItemsCache) {
                if (item.id == id) {
                    mAddonMenuItemsCache.remove(item);
                    break;
                }
            }
        }

        if (mMenu == null)
            return;

        final MenuItem menuItem = mMenu.findItem(id);
        if (menuItem != null)
            mMenu.removeItem(id);
    }

    private void updateAddonMenuItem(int id, final GeckoBundle options) {
        // Set attribute for the menu item in cache, if available
        if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
            for (MenuItemInfo item : mAddonMenuItemsCache) {
                if (item.id == id) {
                    item.label = options.getString("name", item.label);
                    item.checkable = options.getBoolean("checkable", item.checkable);
                    item.checked = options.getBoolean("checked", item.checked);
                    item.enabled = options.getBoolean("enabled", item.enabled);
                    item.visible = options.getBoolean("visible", item.visible);
                    item.added = (mMenu != null);
                    break;
                }
            }
        }

        if (mMenu == null) {
            return;
        }

        final MenuItem menuItem = mMenu.findItem(id);
        if (menuItem != null) {
            menuItem.setTitle(options.getString("name", menuItem.getTitle().toString()));
            menuItem.setCheckable(options.getBoolean("checkable", menuItem.isCheckable()));
            menuItem.setChecked(options.getBoolean("checked", menuItem.isChecked()));
            menuItem.setEnabled(options.getBoolean("enabled", menuItem.isEnabled()));
            menuItem.setVisible(options.getBoolean("visible", menuItem.isVisible()));
        }
    }

    /**
     * Add the provided item to the provided menu, which should be
     * the root (mMenu).
     */
    private void addBrowserActionMenuItemToMenu(final Menu menu, final BrowserActionItemInfo info) {
        info.added = true;

        final MenuItem item = menu.add(Menu.NONE, info.id, Menu.NONE, info.label);

        item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
            @Override
            public boolean onMenuItemClick(MenuItem item) {
                final GeckoBundle data = new GeckoBundle(1);
                data.putString("item", info.uuid);
                EventDispatcher.getInstance().dispatch("Menu:BrowserActionClicked", data);
                return true;
            }
        });

        item.setCheckable(info.checkable);
        item.setChecked(info.checked);
        item.setEnabled(info.enabled);
        item.setVisible(info.visible);
    }

    /**
     * Adds a WebExtension browser action to the menu.
     */
    private void addBrowserActionMenuItem(final BrowserActionItemInfo info) {
        if (mBrowserActionItemsCache == null) {
            mBrowserActionItemsCache = new ArrayList<BrowserActionItemInfo>();
        }

        // Mark it as added if the menu was ready.
        info.added = (mMenu != null);

        // Always cache so we can rebuild after a locale switch.
        mBrowserActionItemsCache.add(info);

        if (mMenu == null) {
            return;
        }

        addBrowserActionMenuItemToMenu(mMenu, info);
    }

    /**
     * Removes a WebExtension browser action from the menu by its UUID.
     */
    private void removeBrowserActionMenuItem(String uuid) {
        int id = -1;

        // Remove browser action menu item from cache, if available.
        if (mBrowserActionItemsCache != null && !mBrowserActionItemsCache.isEmpty()) {
            for (BrowserActionItemInfo item : mBrowserActionItemsCache) {
                if (item.uuid.equals(uuid)) {
                    id = item.id;
                    mBrowserActionItemsCache.remove(item);
                    break;
                }
            }
        }

        if (mMenu == null || id == -1) {
            return;
        }

        final MenuItem menuItem = mMenu.findItem(id);
        if (menuItem != null) {
            mMenu.removeItem(id);
        }
    }

    /**
     * Updates the WebExtension browser action with the specified UUID.
     */
    private void updateBrowserActionMenuItem(String uuid, final GeckoBundle options) {
        int id = -1;

        // Set attribute for the menu item in cache, if available
        if (mBrowserActionItemsCache != null && !mBrowserActionItemsCache.isEmpty()) {
            for (BrowserActionItemInfo item : mBrowserActionItemsCache) {
                if (item.uuid.equals(uuid)) {
                    id = item.id;
                    item.label = options.getString("name", item.label);
                    break;
                }
            }
        }

        if (mMenu == null || id == -1) {
            return;
        }

        final MenuItem menuItem = mMenu.findItem(id);
        if (menuItem != null) {
            menuItem.setTitle(options.getString("name", menuItem.getTitle().toString()));
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Sets mMenu = menu.
        super.onCreateOptionsMenu(menu);

        // Inform the menu about the action-items bar.
        if (menu instanceof GeckoMenu &&
            HardwareUtils.isTablet()) {
            ((GeckoMenu) menu).setActionItemBarPresenter(mBrowserToolbar);
        }

        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.browser_app_menu, mMenu);

        // Add browser action menu items, if any exist.
        if (mBrowserActionItemsCache != null && !mBrowserActionItemsCache.isEmpty()) {
            for (BrowserActionItemInfo item : mBrowserActionItemsCache) {
                addBrowserActionMenuItemToMenu(mMenu, item);
            }
        }

        // Add add-on menu items, if any exist.
        if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
            for (MenuItemInfo item : mAddonMenuItemsCache) {
                addAddonMenuItemToMenu(mMenu, item);
            }
        }

        // Action providers are available only ICS+.
        GeckoMenuItem share = (GeckoMenuItem) mMenu.findItem(R.id.share);

        GeckoActionProvider provider = GeckoActionProvider.getForType(GeckoActionProvider.DEFAULT_MIME_TYPE, this);

        share.setActionProvider(provider);

        return true;
    }

    @Override
    public void openOptionsMenu() {
        hideFirstrunPager(TelemetryContract.Method.MENU);

        // Disable menu access (for hardware buttons) when the software menu button is inaccessible.
        // Note that the software button is always accessible on new tablet.
        if (mBrowserToolbar.isEditing() && !HardwareUtils.isTablet()) {
            return;
        }

        if (ActivityUtils.isFullScreen(this)) {
            return;
        }

        if (areTabsShown()) {
            mTabsPanel.showMenu();
            return;
        }

        // Scroll custom menu to the top
        if (mMenuPanel != null)
            mMenuPanel.scrollTo(0, 0);

        // Scroll menu ListView (potentially in MenuPanel ViewGroup) to top.
        if (mMenu instanceof GeckoMenu) {
            ((GeckoMenu) mMenu).setSelection(0);
        }

        if (!mBrowserToolbar.openOptionsMenu())
            super.openOptionsMenu();

        if (mDynamicToolbar.isEnabled()) {
            mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
        }
    }

    @Override
    public void closeOptionsMenu() {
        if (!mBrowserToolbar.closeOptionsMenu())
            super.closeOptionsMenu();
    }

    @Override // GeckoView.ContentListener
    public void onFullScreen(final GeckoSession session, final boolean fullscreen) {
        super.onFullScreen(session, fullscreen);

        if (fullscreen) {
            mDynamicToolbar.setVisible(false, VisibilityTransition.IMMEDIATE);
            mDynamicToolbar.setPinned(true, PinReason.FULL_SCREEN);
        } else {
            mDynamicToolbar.setPinned(false, PinReason.FULL_SCREEN);
            mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
        }
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu aMenu) {
        if (aMenu == null)
            return false;

        // Hide the tab history panel when hardware menu button is pressed.
        TabHistoryFragment frag = (TabHistoryFragment) getSupportFragmentManager().findFragmentByTag(TAB_HISTORY_FRAGMENT_TAG);
        if (frag != null) {
            frag.dismiss();
        }

        if (!GeckoThread.isRunning()) {
            aMenu.findItem(R.id.settings).setEnabled(false);
            aMenu.findItem(R.id.help).setEnabled(false);
        }

        Tab tab = Tabs.getInstance().getSelectedTab();
        // Unlike other menu items, the bookmark star is not tinted. See {@link ThemedImageButton#setTintedDrawable}.
        final MenuItem bookmark = aMenu.findItem(R.id.bookmark);
        final MenuItem back = aMenu.findItem(R.id.back);
        final MenuItem forward = aMenu.findItem(R.id.forward);
        final MenuItem share = aMenu.findItem(R.id.share);
        final MenuItem bookmarksList = aMenu.findItem(R.id.bookmarks_list);
        final MenuItem historyList = aMenu.findItem(R.id.history_list);
        final MenuItem saveAsPDF = aMenu.findItem(R.id.save_as_pdf);
        final MenuItem print = aMenu.findItem(R.id.print);
        final MenuItem charEncoding = aMenu.findItem(R.id.char_encoding);
        final MenuItem findInPage = aMenu.findItem(R.id.find_in_page);
        final MenuItem desktopMode = aMenu.findItem(R.id.desktop_mode);
        final MenuItem enterGuestMode = aMenu.findItem(R.id.new_guest_session);
        final MenuItem exitGuestMode = aMenu.findItem(R.id.exit_guest_session);

        // Only show the "Quit" menu item on pre-ICS, television devices,
        // or if the user has explicitly enabled the clear on shutdown pref.
        // (We check the pref last to save the pref read.)
        // In ICS+, it's easy to kill an app through the task switcher.
        final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
        final boolean visible = HardwareUtils.isTelevision() ||
                                prefs.getBoolean(GeckoPreferences.PREFS_SHOW_QUIT_MENU, false) ||
                                !PrefUtils.getStringSet(prefs,
                                                        ClearOnShutdownPref.PREF,
                                                        new HashSet<String>()).isEmpty();
        aMenu.findItem(R.id.quit).setVisible(visible);

        // If tab data is unavailable we disable most of the context menu and related items and
        // return early.
        if (tab == null || tab.getURL() == null) {
            bookmark.setEnabled(false);
            back.setEnabled(false);
            forward.setEnabled(false);
            share.setEnabled(false);
            saveAsPDF.setEnabled(false);
            print.setEnabled(false);
            findInPage.setEnabled(false);

            // NOTE: Use MenuUtils.safeSetEnabled because some actions might
            // be on the BrowserToolbar context menu.
            MenuUtils.safeSetEnabled(aMenu, R.id.page, false);
            MenuUtils.safeSetEnabled(aMenu, R.id.subscribe, false);
            MenuUtils.safeSetEnabled(aMenu, R.id.add_search_engine, false);
            MenuUtils.safeSetEnabled(aMenu, R.id.pin_to_top_sites, false);
            MenuUtils.safeSetEnabled(aMenu, R.id.add_to_launcher, false);
            MenuUtils.safeSetEnabled(aMenu, R.id.set_as_homepage, false);

            final MenuItem pinToTopSitesItem = aMenu.findItem(R.id.pin_to_top_sites);
            if (pinToTopSitesItem != null) {
                // This title is set dynamically so we reset it for this edge case.
                pinToTopSitesItem.setTitle(R.string.contextmenu_pin_to_top_sites);
            }

            return true;
        }

        // If tab data IS available we need to manually enable items as necessary. They may have
        // been disabled if returning early above, hence every item must be toggled, even if it's
        // always expected to be enabled (e.g. the bookmark star is always enabled, except when
        // we don't have tab data).

        final boolean inGuestMode = GeckoProfile.get(this).inGuestMode();

        bookmark.setEnabled(true); // Might have been disabled above, ensure it's reenabled
        bookmark.setVisible(!inGuestMode);
        bookmark.setCheckable(true);
        bookmark.setChecked(tab.isBookmark());
        bookmark.setTitle(resolveBookmarkTitleID(tab.isBookmark()));

        final boolean isPrivate = tab.isPrivate();
        // We don't use icons on GB builds so not resolving icons might conserve resources.
        bookmark.setIcon(resolveBookmarkIconDrawable(tab.isBookmark(), resolveMenuIconTint(isPrivate)));

        back.setEnabled(tab.canDoBack());
        forward.setEnabled(tab.canDoForward());
        desktopMode.setChecked(tab.getDesktopMode());

        View backButtonView = MenuItemCompat.getActionView(back);

        if (backButtonView != null) {
            backButtonView.setOnLongClickListener(new Button.OnLongClickListener() {
                @Override
                public boolean onLongClick(View view) {
                    Tab tab = Tabs.getInstance().getSelectedTab();
                    if (tab != null) {
                        closeOptionsMenu();
                        return tabHistoryController.showTabHistory(tab,
                                TabHistoryController.HistoryAction.BACK);
                    }
                    return false;
                }
            });
        }

        View forwardButtonView = MenuItemCompat.getActionView(forward);

        if (forwardButtonView != null) {
            forwardButtonView.setOnLongClickListener(new Button.OnLongClickListener() {
                @Override
                public boolean onLongClick(View view) {
                    Tab tab = Tabs.getInstance().getSelectedTab();
                    if (tab != null) {
                        closeOptionsMenu();
                        return tabHistoryController.showTabHistory(tab,
                                TabHistoryController.HistoryAction.FORWARD);
                    }
                    return false;
                }
            });
        }

        String url = tab.getURL();
        if (AboutPages.isAboutReader(url)) {
            url = ReaderModeUtils.stripAboutReaderUrl(url);
        }

        // Disable share menuitem for about:, chrome:, file:, and resource: URIs
        final boolean shareVisible = Restrictions.isAllowed(this, Restrictable.SHARE);
        share.setVisible(shareVisible);
        final boolean shareEnabled = StringUtils.isShareableUrl(url) && shareVisible;
        share.setEnabled(shareEnabled);
        MenuUtils.safeSetEnabled(aMenu, R.id.downloads, Restrictions.isAllowed(this, Restrictable.DOWNLOAD));

        final boolean distSetAsHomepage = GeckoSharedPrefs.forProfile(this).getBoolean(GeckoPreferences.PREFS_SET_AS_HOMEPAGE, false);
        MenuUtils.safeSetVisible(aMenu, R.id.set_as_homepage, distSetAsHomepage);

        // NOTE: Use MenuUtils.safeSetEnabled because some actions might
        // be on the BrowserToolbar context menu.
        MenuUtils.safeSetEnabled(aMenu, R.id.page, !isAboutHome(tab));
        MenuUtils.safeSetEnabled(aMenu, R.id.subscribe, tab.hasFeeds());
        MenuUtils.safeSetEnabled(aMenu, R.id.add_search_engine, tab.hasOpenSearch());
        MenuUtils.safeSetEnabled(aMenu, R.id.add_to_launcher,
            !isAboutHome(tab) && ShortcutUtils.isPinShortcutSupported());
        MenuUtils.safeSetEnabled(aMenu, R.id.set_as_homepage, !isAboutHome(tab));
        onPrepareOptionsMenuPinToTopSites(aMenu, tab);

        // This provider also applies to the quick share menu item.
        final GeckoActionProvider provider = ((GeckoMenuItem) share).getGeckoActionProvider();
        if (provider != null) {
            Intent shareIntent = provider.getIntent();

            // For efficiency, the provider's intent is only set once
            if (shareIntent == null) {
                shareIntent = new Intent(Intent.ACTION_SEND);
                shareIntent.setType("text/plain");
                provider.setIntent(shareIntent);
            }

            // Replace the existing intent's extras
            shareIntent.putExtra(Intent.EXTRA_TEXT, url);
            shareIntent.putExtra(Intent.EXTRA_SUBJECT, tab.getDisplayTitle());
            shareIntent.putExtra(Intent.EXTRA_TITLE, tab.getDisplayTitle());
            shareIntent.putExtra(ShareDialog.INTENT_EXTRA_DEVICES_ONLY, true);

            // Clear the existing thumbnail extras so we don't share an old thumbnail.
            shareIntent.removeExtra("share_screenshot_uri");

            // Include the thumbnail of the page being shared.
            BitmapDrawable drawable = tab.getThumbnail();
            if (drawable != null) {
                Bitmap thumbnail = drawable.getBitmap();

                // Kobo uses a custom intent extra for sharing thumbnails.
                if (Build.MANUFACTURER.equals("Kobo") && thumbnail != null) {
                    File cacheDir = getExternalCacheDir();

                    if (cacheDir != null) {
                        File outFile = new File(cacheDir, "thumbnail.png");

                        try {
                            final java.io.FileOutputStream out = new java.io.FileOutputStream(outFile);
                            try {
                                thumbnail.compress(Bitmap.CompressFormat.PNG, 90, out);
                            } finally {
                                try {
                                    out.close();
                                } catch (final IOException e) { /* Nothing to do here. */ }
                            }
                        } catch (FileNotFoundException e) {
                            Log.e(LOGTAG, "File not found", e);
                        }

                        shareIntent.putExtra("share_screenshot_uri", Uri.parse(outFile.getPath()));
                    }
                }
            }
        }

        final boolean privateTabVisible = Restrictions.isAllowed(this, Restrictable.PRIVATE_BROWSING);
        MenuUtils.safeSetVisible(aMenu, R.id.new_private_tab, privateTabVisible);

        // Disable PDF generation (save and print) for about:home and xul pages.
        boolean allowPDF = (!(isAboutHome(tab) ||
                               tab.getContentType().equals("application/vnd.mozilla.xul+xml") ||
                               tab.getContentType().startsWith("video/")));
        saveAsPDF.setEnabled(allowPDF);
        print.setEnabled(allowPDF);
        print.setVisible(Versions.feature19Plus);

        // Disable find in page for about:home, since it won't work on Java content.
        findInPage.setEnabled(!isAboutHome(tab));

        charEncoding.setVisible(GeckoPreferences.getCharEncodingState());

        if (getProfile().inGuestMode()) {
            exitGuestMode.setVisible(true);
        } else {
            enterGuestMode.setVisible(true);
        }

        if (!Restrictions.isAllowed(this, Restrictable.GUEST_BROWSING)) {
            MenuUtils.safeSetVisible(aMenu, R.id.new_guest_session, false);
        }

        if (SwitchBoard.isInExperiment(this, Experiments.TOP_ADDONS_MENU)) {
            MenuUtils.safeSetVisible(aMenu, R.id.addons_top_level, true);
            GeckoMenuItem item = (GeckoMenuItem) aMenu.findItem(R.id.addons_top_level);
            if (item != null) {
                if (mExtensionPermissionsHelper.getShowUpdateIcon()) {
                    item.setIcon(R.drawable.ic_addon_update);
                } else {
                    item.setIcon(null);
                }
            }
            MenuUtils.safeSetVisible(aMenu, R.id.addons, false);
        } else {
            MenuUtils.safeSetVisible(aMenu, R.id.addons_top_level, false);
            MenuUtils.safeSetVisible(aMenu, R.id.addons, true);
        }

        if (!Restrictions.isAllowed(this, Restrictable.INSTALL_EXTENSION)) {
            MenuUtils.safeSetVisible(aMenu, R.id.addons, false);
            MenuUtils.safeSetVisible(aMenu, R.id.addons_top_level, false);
        }

        // Hide panel menu items if the panels themselves are hidden.
        // If we don't know whether the panels are hidden, just show the menu items.
        bookmarksList.setVisible(prefs.getBoolean(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED, true));
        historyList.setVisible(prefs.getBoolean(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED, true));

        return true;
    }

    private void onPrepareOptionsMenuPinToTopSites(final Menu aMenu, final Tab tab) {
        final MenuItem item = aMenu.findItem(R.id.pin_to_top_sites);
        if (item == null) {
            return;
        }

        // Set initial state before async query completes.
        item.setEnabled(false); // Disable interaction.
        item.setTitle(R.string.contextmenu_pin_to_top_sites);

        ThreadUtils.postToBackgroundThread(new Runnable() {
            @Override
            public void run() {
                final boolean isPinned = BrowserDB.from(BrowserApp.this).isPinnedForAS(getContentResolver(), tab.getURL());
                ThreadUtils.postToUiThread(new Runnable() {
                    @Override
                    public void run() {
                        item.setTitle(isPinned ?
                                R.string.contextmenu_unpin_from_top_sites : R.string.contextmenu_pin_to_top_sites);
                        item.setEnabled(true);
                    }
                });
            }
        });
    }

    private Drawable resolveBookmarkIconDrawable(final boolean isBookmark, final int tint) {
        if (isBookmark) {
            return ResourcesCompat.getDrawable(getResources(), R.drawable.star_blue, null);
        } else {
            return DrawableUtil.tintDrawable(this, R.drawable.ic_menu_bookmark_add, tint);
        }
    }

    private int resolveMenuIconTint(final boolean isPrivate) {
        final int tintResId;

        if (isPrivate && HardwareUtils.isLargeTablet()) {
            tintResId = R.color.menu_item_tint_private;
        } else {
            tintResId = R.color.menu_item_tint;
        }
        return ResourcesCompat.getColor(getResources(), tintResId, null);
    }

    private int resolveBookmarkTitleID(final boolean isBookmark) {
        return (isBookmark ? R.string.bookmark_remove : R.string.bookmark);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        Tab tab = null;
        Intent intent = null;

        final int itemId = item.getItemId();

        // Track the menu action. We don't know much about the context, but we can use this to determine
        // the frequency of use for various actions.
        String extras = getResources().getResourceEntryName(itemId);
        if (TextUtils.equals(extras, "new_private_tab")) {
            // Mask private browsing
            extras = "new_tab";
        }

        Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, extras);

        mBrowserToolbar.cancelEdit();

        if (itemId == R.id.bookmark) {
            tab = Tabs.getInstance().getSelectedTab();
            if (tab != null) {
                final String extra;
                if (AboutPages.isAboutReader(tab.getURL())) {
                    extra = "bookmark_reader";
                } else {
                    extra = "bookmark";
                }

                final boolean isPrivate = tab.isPrivate();
                if (item.isChecked()) {
                    Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.MENU, extra);
                    tab.removeBookmark();
                    item.setTitle(resolveBookmarkTitleID(false));
                    item.setIcon(resolveBookmarkIconDrawable(false, resolveMenuIconTint(isPrivate)));
                } else {
                    Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, extra);
                    tab.addBookmark();
                    item.setTitle(resolveBookmarkTitleID(true));
                    item.setIcon(resolveBookmarkIconDrawable(true, resolveMenuIconTint(isPrivate)));
                }
            }
            return true;
        }

        if (itemId == R.id.share) {
            tab = Tabs.getInstance().getSelectedTab();
            if (tab != null) {
                String url = tab.getURL();
                if (url != null) {
                    url = ReaderModeUtils.stripAboutReaderUrl(url);

                    // Context: Sharing via chrome list (no explicit session is active)
                    Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "menu");

                    IntentHelper.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, tab.getDisplayTitle(), false);
                }
            }
            return true;
        }

        if (itemId == R.id.reload) {
            tab = Tabs.getInstance().getSelectedTab();
            if (tab != null)
                tab.doReload(false);
            return true;
        }

        if (itemId == R.id.back) {
            tab = Tabs.getInstance().getSelectedTab();
            if (tab != null)
                tab.doBack();
            return true;
        }

        if (itemId == R.id.forward) {
            tab = Tabs.getInstance().getSelectedTab();
            if (tab != null)
                tab.doForward();
            return true;
        }

        if (itemId == R.id.bookmarks_list) {
            final String url = AboutPages.getURLForBuiltinPanelType(PanelType.BOOKMARKS);
            Tabs.getInstance().loadUrl(url);
            return true;
        }

        if (itemId == R.id.history_list) {
            final String url = AboutPages.getURLForBuiltinPanelType(PanelType.COMBINED_HISTORY);
            Tabs.getInstance().loadUrl(url);
            return true;
        }

        if (itemId == R.id.save_as_pdf) {
            Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, "pdf");
            EventDispatcher.getInstance().dispatch("SaveAs:PDF", null);
            return true;
        }

        if (itemId == R.id.print) {
            Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, "print");
            PrintHelper.printPDF(this);
            return true;
        }

        if (itemId == R.id.settings) {
            intent = new Intent(this, GeckoPreferences.class);

            // We want to know when the Settings activity returns, because
            // we might need to redisplay based on a locale change.
            startActivityForResult(intent, ACTIVITY_REQUEST_PREFERENCES);
            return true;
        }

        if (itemId == R.id.help) {
            final String VERSION = AppConstants.MOZ_APP_VERSION;
            final String OS = AppConstants.OS_TARGET;
            final String LOCALE = Locales.getLanguageTag(Locale.getDefault());

            final String URL = getResources().getString(R.string.help_link, VERSION, OS, LOCALE);
            Tabs.getInstance().loadUrlInTab(URL);
            return true;
        }

        if (itemId == R.id.addons || itemId == R.id.addons_top_level) {
            Tabs.getInstance().loadUrlInTab(AboutPages.ADDONS);
            return true;
        }

        if (itemId == R.id.logins) {
            Tabs.getInstance().loadUrlInTab(AboutPages.LOGINS);
            return true;
        }

        if (itemId == R.id.downloads) {
            Tabs.getInstance().loadUrlInTab(AboutPages.DOWNLOADS);
            return true;
        }

        if (itemId == R.id.char_encoding) {
            EventDispatcher.getInstance().dispatch("CharEncoding:Get", null);
            return true;
        }

        if (itemId == R.id.find_in_page) {
            mFindInPageBar.show(mBrowserToolbar.isPrivateMode());
            return true;
        }

        if (itemId == R.id.desktop_mode) {
            Tab selectedTab = Tabs.getInstance().getSelectedTab();
            if (selectedTab == null)
                return true;
            final GeckoBundle args = new GeckoBundle(2);
            args.putBoolean("desktopMode", !item.isChecked());
            args.putInt("tabId", selectedTab.getId());
            EventDispatcher.getInstance().dispatch("DesktopMode:Change", args);
            return true;
        }

        if (itemId == R.id.new_tab) {
            addTab();
            return true;
        }

        if (itemId == R.id.new_private_tab) {
            addPrivateTab();
            return true;
        }

        if (itemId == R.id.new_guest_session) {
            showGuestModeDialog(GuestModeDialog.ENTERING);
            return true;
        }

        if (itemId == R.id.exit_guest_session) {
            showGuestModeDialog(GuestModeDialog.LEAVING);
            return true;
        }

        // We have a few menu items that can also be in the context menu. If
        // we have not already handled the item, give the context menu handler
        // a chance.
        if (onContextItemSelected(item)) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    public boolean onMenuItemLongClick(MenuItem item) {
        if (item.getItemId() == R.id.reload) {
            Tab tab = Tabs.getInstance().getSelectedTab();
            if (tab != null) {
                tab.doReload(true);

                Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "reload_force");
            }
            return true;
        }

        return super.onMenuItemLongClick(item);
    }

    public void showGuestModeDialog(final GuestModeDialog type) {
        if ((type == GuestModeDialog.ENTERING) == getProfile().inGuestMode()) {
            // Don't show enter dialog if we are already in guest mode; same with leaving.
            return;
        }

        final Prompt ps = new Prompt(this, new Prompt.PromptCallback() {
            @Override
            public void onPromptFinished(final GeckoBundle result) {
                final int itemId = result.getInt("button", -1);
                if (itemId != 0) {
                    return;
                }

                final Context context = GeckoAppShell.getApplicationContext();
                if (type == GuestModeDialog.ENTERING) {
                    GeckoProfile.enterGuestMode(context);
                } else {
                    GeckoProfile.leaveGuestMode(context);
                    // Now's a good time to make sure we're not displaying the
                    // Guest Browsing notification.
                    GuestSession.hideNotification(context);
                }
                finishAndShutdown(/* restart */ true);
            }
        });

        Resources res = getResources();
        ps.setButtons(new String[] {
            res.getString(R.string.guest_session_dialog_continue),
            res.getString(R.string.guest_session_dialog_cancel)
        });

        int titleString = 0;
        int msgString = 0;
        if (type == GuestModeDialog.ENTERING) {
            titleString = R.string.new_guest_session_title;
            msgString = R.string.new_guest_session_text;
        } else {
            titleString = R.string.exit_guest_session_title;
            msgString = R.string.exit_guest_session_text;
        }

        ps.show(res.getString(titleString), res.getString(msgString), null, ListView.CHOICE_MODE_NONE);
    }

    /**
     * Handle a long press on the back button
     */
    private boolean handleBackLongPress() {
        // If the tab search history is already shown, do nothing.
        TabHistoryFragment frag = (TabHistoryFragment) getSupportFragmentManager().findFragmentByTag(TAB_HISTORY_FRAGMENT_TAG);
        if (frag != null) {
            return true;
        }

        Tab tab = Tabs.getInstance().getSelectedTab();
        if (tab != null  && !tab.isEditing()) {
            return tabHistoryController.showTabHistory(tab, TabHistoryController.HistoryAction.ALL);
        }

        return false;
    }

    /**
     * This will detect if the key pressed is back. If so, will show the history.
     */
    @Override
    public boolean onKeyLongPress(int keyCode, KeyEvent event) {
        // onKeyLongPress is broken in Android N, see onKeyDown() for more information. We add a version
        // check here to match our fallback code in order to avoid handling a long press twice (which
        // could happen if newer versions of android and/or other vendors were to  fix this problem).
        if (Versions.preN &&
                keyCode == KeyEvent.KEYCODE_BACK) {
            if (handleBackLongPress()) {
                return true;
            }

        }
        return super.onKeyLongPress(keyCode, event);
    }

    /*
     * If the app has been launched a certain number of times, and we haven't asked for feedback before,
     * open a new tab with about:feedback when launching the app from the icon shortcut.
     */
    @Override
    protected void onNewIntent(Intent externalIntent) {
        final SafeIntent intent = new SafeIntent(externalIntent);
        String action = intent.getAction();

        final boolean isViewAction = Intent.ACTION_VIEW.equals(action);
        final boolean isBookmarkAction = GeckoApp.ACTION_HOMESCREEN_SHORTCUT.equals(action);
        final boolean isTabQueueAction = TabQueueHelper.LOAD_URLS_ACTION.equals(action);
        final boolean isViewMultipleAction = ACTION_VIEW_MULTIPLE.equals(action);

        if (mInitialized && (isViewAction || isBookmarkAction)) {
            // Dismiss editing mode if the user is loading a URL from an external app.
            mBrowserToolbar.cancelEdit();

            // Hide firstrun-pane if the user is loading a URL from an external app.
            hideFirstrunPager(TelemetryContract.Method.NONE);

            if (isBookmarkAction) {
                // GeckoApp.ACTION_HOMESCREEN_SHORTCUT means we're opening a bookmark that
                // was added to Android's homescreen.
                Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.HOMESCREEN);
            }
        }

        showTabQueuePromptIfApplicable(intent);

        // GeckoApp will wrap this unsafe external intent in a SafeIntent.
        super.onNewIntent(externalIntent);

        if (AppConstants.MOZ_ANDROID_BEAM && NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
            final GeckoBundle data = new GeckoBundle(2);
            data.putString("uri", intent.getDataString());
            data.putString("flags", "OPEN_NEWTAB");
            getAppEventDispatcher().dispatch("Tab:OpenUri", data);
        }

        // Only solicit feedback when the app has been launched from the icon shortcut.
        if (GuestSession.NOTIFICATION_INTENT.equals(action)) {
            GuestSession.onNotificationIntentReceived(this);
        }

        // If the user has clicked the tab queue notification then load the tabs.
        if (TabQueueHelper.TAB_QUEUE_ENABLED && mInitialized && isTabQueueAction) {
            Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, "tabqueue");
            ThreadUtils.postToBackgroundThread(new Runnable() {
                @Override
                public void run() {
                    openQueuedTabs();
                }
            });
        }

        // Custom intent action for opening multiple URLs at once
        if (isViewMultipleAction) {
            openMultipleTabsFromIntent(intent);
        }

        for (final BrowserAppDelegate delegate : delegates) {
            delegate.onNewIntent(this, intent);
        }

        if (!mInitialized || !Intent.ACTION_MAIN.equals(action)) {
            return;
        }

        // Check to see how many times the app has been launched.
        final String keyName = getPackageName() + ".feedback_launch_count";
        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();

        // Faster on main thread with an async apply().
        try {
            SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
            int launchCount = settings.getInt(keyName, 0);
            if (launchCount < FEEDBACK_LAUNCH_COUNT) {
                // Increment the launch count and store the new value.
                launchCount++;
                settings.edit().putInt(keyName, launchCount).apply();

                // If we've reached our magic number, show the feedback page.
                if (launchCount == FEEDBACK_LAUNCH_COUNT) {
                    EventDispatcher.getInstance().dispatch("Feedback:Show", null);
                }
            }
        } finally {
            StrictMode.setThreadPolicy(savedPolicy);
        }
    }

    public void openUrls(List<String> urls) {
        final GeckoBundle data = new GeckoBundle(1);
        data.putStringArray("urls", urls.toArray(new String[urls.size()]));
        EventDispatcher.getInstance().dispatch("Tabs:OpenMultiple", data);
    }

    private void showTabQueuePromptIfApplicable(final SafeIntent intent) {
        ThreadUtils.postToBackgroundThread(new Runnable() {
            @Override
            public void run() {
                // We only want to show the prompt if the browser has been opened from an external url
                if (TabQueueHelper.TAB_QUEUE_ENABLED && mInitialized
                                                     && Intent.ACTION_VIEW.equals(intent.getAction())
                                                     && !intent.getBooleanExtra(BrowserContract.SKIP_TAB_QUEUE_FLAG, false)
                                                     && TabQueueHelper.shouldShowTabQueuePrompt(BrowserApp.this)) {
                    Intent promptIntent = new Intent(BrowserApp.this, TabQueuePrompt.class);
                    startActivityForResult(promptIntent, ACTIVITY_REQUEST_TAB_QUEUE);
                }
            }
        });
    }

    // HomePager.OnUrlOpenListener
    @Override
    public void onUrlOpen(String url, EnumSet<OnUrlOpenListener.Flags> flags) {
        onUrlOpenWithReferrer(url, null, flags);
    }

    @Override
    public void onUrlOpenWithReferrer(final String url, @Nullable final String referrerUri,
            final EnumSet<OnUrlOpenListener.Flags> flags) {
        if (flags.contains(OnUrlOpenListener.Flags.OPEN_WITH_INTENT)) {
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setData(Uri.parse(url));
            startActivity(intent);
        } else {
            // By default this listener is used for lists where the offline reader-view icon
            // is shown - hence we need to redirect to the reader-view page by default.
            // However there are some cases where we might not want to use this, e.g.
            // for topsites where we do not indicate that a page is an offline reader-view bookmark too.
            final String pageURL;
            if (!flags.contains(OnUrlOpenListener.Flags.NO_READER_VIEW)) {
                pageURL = SavedReaderViewHelper.getReaderURLIfCached(this, url);
            } else {
                pageURL = url;
            }

            if (!maybeSwitchToTab(pageURL, flags)) {
                openUrlAndStopEditingWithReferrer(pageURL, referrerUri);
                clearSelectedTabApplicationId();
            }
        }
    }

    // HomePager.OnUrlOpenInBackgroundListener
    @Override
    public void onUrlOpenInBackground(final String url, EnumSet<OnUrlOpenInBackgroundListener.Flags> flags) {
        onUrlOpenInBackgroundWithReferrer(url, null, flags);
    }

    @Override
    public void onUrlOpenInBackgroundWithReferrer(final String url, @Nullable final String referrerUri,
            final EnumSet<OnUrlOpenInBackgroundListener.Flags> flags) {
        if (url == null) {
            throw new IllegalArgumentException("url must not be null");
        }
        if (flags == null) {
            throw new IllegalArgumentException("flags must not be null");
        }

        // We only use onUrlOpenInBackgroundListener for the homepanel context menus, hence
        // we should always be checking whether we want the readermode version
        final String pageURL = SavedReaderViewHelper.getReaderURLIfCached(this, url);

        final boolean isPrivate = flags.contains(OnUrlOpenInBackgroundListener.Flags.PRIVATE);

        int loadFlags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_BACKGROUND;
        if (isPrivate) {
            loadFlags |= Tabs.LOADURL_PRIVATE;
        }

        final Tab newTab = Tabs.getInstance().loadUrl(pageURL, null, referrerUri, Tabs.INVALID_TAB_ID, null, loadFlags);

        // We switch to the desired tab by unique ID, which closes any window
        // for a race between opening the tab and closing it, and switching to
        // it. We could also switch to the Tab explicitly, but we don't want to
        // hold a reference to the Tab itself in the anonymous listener class.
        final int newTabId = newTab.getId();

        final SnackbarBuilder.SnackbarCallback callback = new SnackbarBuilder.SnackbarCallback() {
            @Override
            public void onClick(View v) {
                Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.TOAST, "switchtab");

                maybeSwitchToTab(newTabId);
            }
        };

        final String message = isPrivate ?
                getResources().getString(R.string.new_private_tab_opened) :
                getResources().getString(R.string.new_tab_opened);
        final String buttonMessage = getResources().getString(R.string.switch_button_message);

        SnackbarBuilder.builder(this)
                .message(message)
                .duration(Snackbar.LENGTH_LONG)
                .action(buttonMessage)
                .callback(callback)
                .buildAndShow();
    }

    // BrowserSearch.OnSearchListener
    @Override
    public void onSearch(SearchEngine engine, final String text, final TelemetryContract.Method method) {
        // Don't store searches that happen in private tabs. This assumes the user can only
        // perform a search inside the currently selected tab, which is true for searches
        // that come from SearchEngineRow.
        if (!mBrowserToolbar.isPrivateMode()) {
            storeSearchQuery(text);
        }

        // We don't use SearchEngine.getEngineIdentifier because it can
        // return a custom search engine name, which is a privacy concern.
        final String identifierToRecord = (engine.identifier != null) ? engine.identifier : "other";
        recordSearch(GeckoSharedPrefs.forProfile(this), identifierToRecord, method);
        openUrlAndStopEditing(text, engine.name);
    }

    // BrowserSearch.OnEditSuggestionListener
    @Override
    public void onEditSuggestion(String suggestion) {
        mBrowserToolbar.onEditSuggestion(suggestion);
    }

    @Override
    public int getLayout() { return R.layout.gecko_app; }

    @Override
    public View getDoorhangerOverlay() {
        return doorhangerOverlay;
    }

    public SearchEngineManager getSearchEngineManager() {
        return mSearchEngineManager;
    }

    // For use from tests only.
    @RobocopTarget
    public ReadingListHelper getReadingListHelper() {
        return mReadingListHelper;
    }

    @Override
    protected ActionModePresenter getTextSelectPresenter() {
        return this;
    }


    /* Implementing ActionModeCompat.Presenter */
    @Override
    public void startActionMode(final ActionModeCompat.Callback callback) {
        // If actionMode is null, we're not currently showing one. Flip to the action mode view
        if (mActionMode == null) {
            mActionBarFlipper.showNext();
            DynamicToolbarAnimator toolbar = mLayerView.getDynamicToolbarAnimator();

            // If the toolbar is dynamic and not currently showing, just show the real toolbar
            // and keep the animated snapshot hidden
            if (mDynamicToolbar.isEnabled() && !isToolbarChromeVisible()) {
                toggleToolbarChrome(true);
                mShowingToolbarChromeForActionBar = 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();
        }
        mActionMode.animateIn();
    }

    /* Implementing ActionModeCompat.Presenter */
    @Override
    public void endActionMode() {
        if (mActionMode == null) {
            return;
        }

        mActionMode.finish();
        mActionMode = null;
        mDynamicToolbar.setPinned(false, PinReason.ACTION_MODE);

        mActionBarFlipper.showPrevious();

        // Hide the real toolbar chrome if it was hidden before the action bar
        // was shown.
        if (mShowingToolbarChromeForActionBar) {
            toggleToolbarChrome(false);
            mShowingToolbarChromeForActionBar = false;
        }
    }

    public static interface TabStripInterface {
        public void refresh();
        /** Called to let the tab strip know it is now, or is now no longer, being hidden by
         *  something being drawn over it.
         */
        void tabStripIsCovered(boolean covered);
        void setOnTabChangedListener(OnTabAddedOrRemovedListener listener);
        interface OnTabAddedOrRemovedListener {
            void onTabChanged();
        }
    }

    @Override
    protected void recordStartupActionTelemetry(final String passedURL, final String action) {
        final TelemetryContract.Method method;
        if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
            // This action is also recorded via "loadurl.1" > "homescreen".
            method = TelemetryContract.Method.HOMESCREEN;
        } else if (passedURL == null) {
            Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, TelemetryContract.Method.HOMESCREEN, "launcher");
            method = TelemetryContract.Method.HOMESCREEN;
        } else {
            // This is action is also recorded via "loadurl.1" > "intent".
            method = TelemetryContract.Method.INTENT;
        }

        if (GeckoProfile.get(this).inGuestMode()) {
            Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, method, "guest");
        } else if (Restrictions.isRestrictedProfile(this)) {
            Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, method, "restricted");
        }
    }

    /**
     * Launch edit bookmark dialog. The {@link BookmarkEditFragment} needs to be started by an activity
     * that implements the interface({@link BookmarkEditFragment.Callbacks}) for handling callback method.
     */
    public void showEditBookmarkDialog(String pageUrl) {
        if (BookmarkUtils.isEnabled(this)) {
            BookmarkEditFragment dialog = BookmarkEditFragment.newInstance(pageUrl);
            dialog.show(getSupportFragmentManager(), "edit-bookmark");
        } else {
            new EditBookmarkDialog(this).show(pageUrl);
        }
    }

    @Override
    public void onEditBookmark(@NonNull Bundle bundle) {
        new EditBookmarkTask(this, bundle).execute();
    }

    @Override
    public void onLightweightThemeChanged() {
        refreshStatusBarColor();
    }

    @Override
    public void onLightweightThemeReset() {
        refreshStatusBarColor();
    }

    private void refreshStatusBarColor() {
        final boolean isPrivate = mBrowserToolbar.isPrivateMode();
        WindowUtil.setStatusBarColor(BrowserApp.this, isPrivate);
    }
}
