diff -r 000000000000 -r 6474c204b198 mobile/android/base/toolbar/BrowserToolbar.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/toolbar/BrowserToolbar.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,1451 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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.toolbar; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; + +import org.json.JSONObject; +import org.mozilla.gecko.BrowserApp; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoApplication; +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.LightweightTheme; +import org.mozilla.gecko.R; +import org.mozilla.gecko.Tab; +import org.mozilla.gecko.Tabs; +import org.mozilla.gecko.Telemetry; +import org.mozilla.gecko.TelemetryContract; +import org.mozilla.gecko.animation.PropertyAnimator; +import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener; +import org.mozilla.gecko.animation.ViewHelper; +import org.mozilla.gecko.menu.GeckoMenu; +import org.mozilla.gecko.menu.MenuPopup; +import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.OnStopListener; +import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.OnTitleChangeListener; +import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.UpdateFlags; +import org.mozilla.gecko.util.Clipboard; +import org.mozilla.gecko.util.GeckoEventListener; +import org.mozilla.gecko.util.HardwareUtils; +import org.mozilla.gecko.util.MenuUtils; +import org.mozilla.gecko.widget.ThemedImageButton; +import org.mozilla.gecko.widget.ThemedImageView; +import org.mozilla.gecko.widget.ThemedRelativeLayout; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; +import android.os.Build; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.PopupWindow; +import android.widget.RelativeLayout; + +/** +* {@code BrowserToolbar} is single entry point for users of the toolbar +* subsystem i.e. this should be the only import outside the 'toolbar' +* package. +* +* {@code BrowserToolbar} serves at the single event bus for all +* sub-components in the toolbar. It tracks tab events and gecko messages +* and update the state of its inner components accordingly. +* +* It has two states, display and edit, which are controlled by +* ToolbarEditLayout and ToolbarDisplayLayout. In display state, the toolbar +* displays the current state for the selected tab. In edit state, it shows +* a text entry for searching bookmarks/history. {@code BrowserToolbar} +* provides public API to enter, cancel, and commit the edit state as well +* as a set of listeners to allow {@code BrowserToolbar} users to react +* to state changes accordingly. +*/ +public class BrowserToolbar extends ThemedRelativeLayout + implements Tabs.OnTabsChangedListener, + GeckoMenu.ActionItemBarPresenter, + GeckoEventListener { + private static final String LOGTAG = "GeckoToolbar"; + + public interface OnActivateListener { + public void onActivate(); + } + + public interface OnCommitListener { + public void onCommit(); + } + + public interface OnDismissListener { + public void onDismiss(); + } + + public interface OnFilterListener { + public void onFilter(String searchText, AutocompleteHandler handler); + } + + public interface OnStartEditingListener { + public void onStartEditing(); + } + + public interface OnStopEditingListener { + public void onStopEditing(); + } + + private enum UIMode { + EDIT, + DISPLAY + } + + enum ForwardButtonAnimation { + SHOW, + HIDE + } + + private ToolbarDisplayLayout urlDisplayLayout; + private ToolbarEditLayout urlEditLayout; + private View urlBarEntry; + private RelativeLayout.LayoutParams urlBarEntryDefaultLayoutParams; + private RelativeLayout.LayoutParams urlBarEntryShrunkenLayoutParams; + private ImageView urlBarTranslatingEdge; + private boolean isSwitchingTabs; + private ShapedButton tabsButton; + private ImageButton backButton; + private ImageButton forwardButton; + + private ToolbarProgressView progressBar; + private TabCounter tabsCounter; + private ThemedImageButton menuButton; + private ThemedImageView menuIcon; + private LinearLayout actionItemBar; + private MenuPopup menuPopup; + private List focusOrder; + + private final ThemedImageView editCancel; + + private boolean shouldShrinkURLBar = false; + + private OnActivateListener activateListener; + private OnFocusChangeListener focusChangeListener; + private OnStartEditingListener startEditingListener; + private OnStopEditingListener stopEditingListener; + + private final BrowserApp activity; + private boolean hasSoftMenuButton; + + private UIMode uiMode; + private boolean isAnimatingEntry; + + private int urlBarViewOffset; + private int defaultForwardMargin; + + private static final Interpolator buttonsInterpolator = new AccelerateInterpolator(); + + private static final int FORWARD_ANIMATION_DURATION = 450; + + private final LightweightTheme theme; + + public BrowserToolbar(Context context) { + this(context, null); + } + + public BrowserToolbar(Context context, AttributeSet attrs) { + super(context, attrs); + theme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme(); + + // BrowserToolbar is attached to BrowserApp only. + activity = (BrowserApp) context; + + // Inflate the content. + LayoutInflater.from(context).inflate(R.layout.browser_toolbar, this); + + Tabs.registerOnTabsChangedListener(this); + isSwitchingTabs = true; + isAnimatingEntry = false; + + registerEventListener("Reader:Click"); + registerEventListener("Reader:LongClick"); + + final Resources res = getResources(); + urlBarViewOffset = res.getDimensionPixelSize(R.dimen.url_bar_offset_left); + defaultForwardMargin = res.getDimensionPixelSize(R.dimen.forward_default_offset); + urlDisplayLayout = (ToolbarDisplayLayout) findViewById(R.id.display_layout); + urlBarEntry = findViewById(R.id.url_bar_entry); + urlEditLayout = (ToolbarEditLayout) findViewById(R.id.edit_layout); + + urlBarEntryDefaultLayoutParams = (RelativeLayout.LayoutParams) urlBarEntry.getLayoutParams(); + // API level 19 adds a RelativeLayout.LayoutParams copy constructor, so we explicitly cast + // to ViewGroup.MarginLayoutParams to ensure consistency across platforms. + urlBarEntryShrunkenLayoutParams = new RelativeLayout.LayoutParams( + (ViewGroup.MarginLayoutParams) urlBarEntryDefaultLayoutParams); + urlBarEntryShrunkenLayoutParams.addRule(RelativeLayout.ALIGN_RIGHT, R.id.edit_layout); + urlBarEntryShrunkenLayoutParams.rightMargin = 0; + + // This will clip the translating edge's image at 60% of its width + urlBarTranslatingEdge = (ImageView) findViewById(R.id.url_bar_translating_edge); + if (urlBarTranslatingEdge != null) { + urlBarTranslatingEdge.getDrawable().setLevel(6000); + } + + tabsButton = (ShapedButton) findViewById(R.id.tabs); + tabsCounter = (TabCounter) findViewById(R.id.tabs_counter); + if (Build.VERSION.SDK_INT >= 11) { + tabsCounter.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + + backButton = (ImageButton) findViewById(R.id.back); + setButtonEnabled(backButton, false); + forwardButton = (ImageButton) findViewById(R.id.forward); + setButtonEnabled(forwardButton, false); + + menuButton = (ThemedImageButton) findViewById(R.id.menu); + menuIcon = (ThemedImageView) findViewById(R.id.menu_icon); + actionItemBar = (LinearLayout) findViewById(R.id.menu_items); + hasSoftMenuButton = !HardwareUtils.hasMenuButton(); + + editCancel = (ThemedImageView) findViewById(R.id.edit_cancel); + + // We use different layouts on phones and tablets, so adjust the focus + // order appropriately. + focusOrder = new ArrayList(); + if (HardwareUtils.isTablet()) { + focusOrder.addAll(Arrays.asList(tabsButton, backButton, forwardButton, this)); + focusOrder.addAll(urlDisplayLayout.getFocusOrder()); + focusOrder.addAll(Arrays.asList(actionItemBar, menuButton)); + } else { + focusOrder.add(this); + focusOrder.addAll(urlDisplayLayout.getFocusOrder()); + focusOrder.addAll(Arrays.asList(tabsButton, menuButton)); + } + + setUIMode(UIMode.DISPLAY); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + setOnClickListener(new Button.OnClickListener() { + @Override + public void onClick(View v) { + if (activateListener != null) { + activateListener.onActivate(); + } + } + }); + + setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + // We don't the context menu while editing + if (isEditing()) { + return; + } + + // NOTE: Use MenuUtils.safeSetVisible because some actions might + // be on the Page menu + + MenuInflater inflater = activity.getMenuInflater(); + inflater.inflate(R.menu.titlebar_contextmenu, menu); + + String clipboard = Clipboard.getText(); + if (TextUtils.isEmpty(clipboard)) { + menu.findItem(R.id.pasteandgo).setVisible(false); + menu.findItem(R.id.paste).setVisible(false); + } + + Tab tab = Tabs.getInstance().getSelectedTab(); + if (tab != null) { + String url = tab.getURL(); + if (url == null) { + menu.findItem(R.id.copyurl).setVisible(false); + menu.findItem(R.id.add_to_launcher).setVisible(false); + MenuUtils.safeSetVisible(menu, R.id.share, false); + } + + MenuUtils.safeSetVisible(menu, R.id.subscribe, tab.hasFeeds()); + MenuUtils.safeSetVisible(menu, R.id.add_search_engine, tab.hasOpenSearch()); + } else { + // if there is no tab, remove anything tab dependent + menu.findItem(R.id.copyurl).setVisible(false); + menu.findItem(R.id.add_to_launcher).setVisible(false); + MenuUtils.safeSetVisible(menu, R.id.share, false); + MenuUtils.safeSetVisible(menu, R.id.subscribe, false); + MenuUtils.safeSetVisible(menu, R.id.add_search_engine, false); + } + + MenuUtils.safeSetVisible(menu, R.id.share, !GeckoProfile.get(getContext()).inGuestMode()); + } + }); + + urlDisplayLayout.setOnStopListener(new OnStopListener() { + @Override + public Tab onStop() { + final Tab tab = Tabs.getInstance().getSelectedTab(); + if (tab != null) { + tab.doStop(); + return tab; + } + + return null; + } + }); + + urlDisplayLayout.setOnTitleChangeListener(new OnTitleChangeListener() { + @Override + public void onTitleChange(CharSequence title) { + final String contentDescription; + if (title != null) { + contentDescription = title.toString(); + } else { + contentDescription = activity.getString(R.string.url_bar_default_text); + } + + // The title and content description should + // always be sync. + setContentDescription(contentDescription); + } + }); + + urlEditLayout.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + // This will select the url bar when entering editing mode. + setSelected(hasFocus); + if (focusChangeListener != null) { + focusChangeListener.onFocusChange(v, hasFocus); + } + } + }); + + tabsButton.setOnClickListener(new Button.OnClickListener() { + @Override + public void onClick(View v) { + toggleTabs(); + } + }); + tabsButton.setImageLevel(0); + + backButton.setOnClickListener(new Button.OnClickListener() { + @Override + public void onClick(View view) { + Tabs.getInstance().getSelectedTab().doBack(); + } + }); + backButton.setOnLongClickListener(new Button.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + return Tabs.getInstance().getSelectedTab().showBackHistory(); + } + }); + + forwardButton.setOnClickListener(new Button.OnClickListener() { + @Override + public void onClick(View view) { + Tabs.getInstance().getSelectedTab().doForward(); + } + }); + forwardButton.setOnLongClickListener(new Button.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + return Tabs.getInstance().getSelectedTab().showForwardHistory(); + } + }); + + if (editCancel != null) { + editCancel.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + // If we exit editing mode during the animation, + // we're put into an inconsistent state (bug 1017276). + if (!isAnimatingEntry) { + Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, + TelemetryContract.Method.ACTIONBAR, + getResources().getResourceEntryName(editCancel.getId())); + cancelEdit(); + } + } + }); + } + + if (hasSoftMenuButton) { + menuButton.setVisibility(View.VISIBLE); + menuIcon.setVisibility(View.VISIBLE); + + menuButton.setOnClickListener(new Button.OnClickListener() { + @Override + public void onClick(View view) { + activity.openOptionsMenu(); + } + }); + } + } + + public void setProgressBar(ToolbarProgressView progressBar) { + this.progressBar = progressBar; + } + + public void refresh() { + urlDisplayLayout.dismissSiteIdentityPopup(); + } + + public boolean onBackPressed() { + // If we exit editing mode during the animation, + // we're put into an inconsistent state (bug 1017276). + if (isEditing() && !isAnimatingEntry) { + Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, + TelemetryContract.Method.BACK); + cancelEdit(); + return true; + } + + return urlDisplayLayout.dismissSiteIdentityPopup(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // If the motion event has occured below the toolbar (due to the scroll + // offset), let it pass through to the page. + if (event != null && event.getY() > getHeight() + ViewHelper.getTranslationY(this)) { + return false; + } + + return super.onTouchEvent(event); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + if (h != oldh) { + // Post this to happen outside of onSizeChanged, as this may cause + // a layout change and relayouts within a layout change don't work. + post(new Runnable() { + @Override + public void run() { + activity.refreshToolbarHeight(); + } + }); + } + } + + @Override + public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) { + Log.d(LOGTAG, "onTabChanged: " + msg); + final Tabs tabs = Tabs.getInstance(); + + // These conditions are split into three phases: + // * Always do first + // * Handling specific to the selected tab + // * Always do afterwards. + + switch (msg) { + case ADDED: + case CLOSED: + updateTabCount(tabs.getDisplayCount()); + break; + case RESTORED: + // TabCount fixup after OOM + case SELECTED: + urlDisplayLayout.dismissSiteIdentityPopup(); + updateTabCount(tabs.getDisplayCount()); + isSwitchingTabs = true; + break; + } + + if (tabs.isSelectedTab(tab)) { + final EnumSet flags = EnumSet.noneOf(UpdateFlags.class); + + // Progress-related handling + switch (msg) { + case START: + updateProgressVisibility(tab, Tab.LOAD_PROGRESS_INIT); + // Fall through. + case ADDED: + case LOCATION_CHANGE: + case LOAD_ERROR: + case LOADED: + case STOP: + flags.add(UpdateFlags.PROGRESS); + if (progressBar.getVisibility() == View.VISIBLE) { + progressBar.animateProgress(tab.getLoadProgress()); + } + break; + + case SELECTED: + flags.add(UpdateFlags.PROGRESS); + updateProgressVisibility(); + break; + } + + switch (msg) { + case STOP: + // Reset the title in case we haven't navigated + // to a new page yet. + flags.add(UpdateFlags.TITLE); + // Fall through. + case START: + case CLOSED: + case ADDED: + updateBackButton(tab); + updateForwardButton(tab); + break; + + case SELECTED: + flags.add(UpdateFlags.PRIVATE_MODE); + setPrivateMode(tab.isPrivate()); + // Fall through. + case LOAD_ERROR: + flags.add(UpdateFlags.TITLE); + // Fall through. + case LOCATION_CHANGE: + // A successful location change will cause Tab to notify + // us of a title change, so we don't update the title here. + flags.add(UpdateFlags.FAVICON); + flags.add(UpdateFlags.SITE_IDENTITY); + + updateBackButton(tab); + updateForwardButton(tab); + break; + + case TITLE: + flags.add(UpdateFlags.TITLE); + break; + + case FAVICON: + flags.add(UpdateFlags.FAVICON); + break; + + case SECURITY_CHANGE: + flags.add(UpdateFlags.SITE_IDENTITY); + break; + } + + if (!flags.isEmpty()) { + updateDisplayLayout(tab, flags); + } + } + + switch (msg) { + case SELECTED: + case LOAD_ERROR: + case LOCATION_CHANGE: + isSwitchingTabs = false; + } + } + + private void updateProgressVisibility() { + final Tab selectedTab = Tabs.getInstance().getSelectedTab(); + updateProgressVisibility(selectedTab, selectedTab.getLoadProgress()); + } + + private void updateProgressVisibility(Tab selectedTab, int progress) { + if (!isEditing() && selectedTab.getState() == Tab.STATE_LOADING) { + progressBar.setProgress(progress); + progressBar.setVisibility(View.VISIBLE); + } else { + progressBar.setVisibility(View.GONE); + } + } + + private boolean isVisible() { + return ViewHelper.getTranslationY(this) == 0; + } + + @Override + public void setNextFocusDownId(int nextId) { + super.setNextFocusDownId(nextId); + tabsButton.setNextFocusDownId(nextId); + backButton.setNextFocusDownId(nextId); + forwardButton.setNextFocusDownId(nextId); + urlDisplayLayout.setNextFocusDownId(nextId); + menuButton.setNextFocusDownId(nextId); + } + + private int getUrlBarEntryTranslation() { + if (editCancel == null) { + // We are on tablet, and there is no animation so return a translation of 0. + return 0; + } + + // Find the distance from the right-edge of the url bar (where we're translating from) to + // the left-edge of the cancel button (where we're translating to; note that the cancel + // button must be laid out, i.e. not View.GONE). + final LayoutParams lp = (LayoutParams) urlEditLayout.getLayoutParams(); + return editCancel.getLeft() - lp.leftMargin - urlBarEntry.getRight(); + } + + private int getUrlBarCurveTranslation() { + return getWidth() - tabsButton.getLeft(); + } + + private boolean canDoBack(Tab tab) { + return (tab.canDoBack() && !isEditing()); + } + + private boolean canDoForward(Tab tab) { + return (tab.canDoForward() && !isEditing()); + } + + private void addTab() { + activity.addTab(); + } + + private void toggleTabs() { + if (activity.areTabsShown()) { + if (activity.hasTabsSideBar()) + activity.hideTabs(); + } else { + // hide the virtual keyboard + InputMethodManager imm = + (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(tabsButton.getWindowToken(), 0); + + Tab tab = Tabs.getInstance().getSelectedTab(); + if (tab != null) { + if (!tab.isPrivate()) + activity.showNormalTabs(); + else + activity.showPrivateTabs(); + } + } + } + + private void updateTabCountAndAnimate(int count) { + // Don't animate if the toolbar is hidden. + if (!isVisible()) { + updateTabCount(count); + return; + } + + // If toolbar is in edit mode on a phone, this means the entry is expanded + // and the tabs button is translated offscreen. Don't trigger tabs counter + // updates until the tabs button is back on screen. + // See stopEditing() + if (!isEditing() || HardwareUtils.isTablet()) { + tabsCounter.setCount(count); + + tabsButton.setContentDescription((count > 1) ? + activity.getString(R.string.num_tabs, count) : + activity.getString(R.string.one_tab)); + } + } + + private void updateTabCount(int count) { + // If toolbar is in edit mode on a phone, this means the entry is expanded + // and the tabs button is translated offscreen. Don't trigger tabs counter + // updates until the tabs button is back on screen. + // See stopEditing() + if (isEditing() && !HardwareUtils.isTablet()) { + return; + } + + // Set TabCounter based on visibility + if (isVisible() && ViewHelper.getAlpha(tabsCounter) != 0 && !isEditing()) { + tabsCounter.setCountWithAnimation(count); + } else { + tabsCounter.setCount(count); + } + + // Update A11y information + tabsButton.setContentDescription((count > 1) ? + activity.getString(R.string.num_tabs, count) : + activity.getString(R.string.one_tab)); + } + + private void updateDisplayLayout(Tab tab, EnumSet flags) { + if (isSwitchingTabs) { + flags.add(UpdateFlags.DISABLE_ANIMATIONS); + } + + urlDisplayLayout.updateFromTab(tab, flags); + + if (flags.contains(UpdateFlags.TITLE)) { + if (!isEditing()) { + urlEditLayout.setText(tab.getURL()); + } + } + + if (flags.contains(UpdateFlags.PROGRESS)) { + updateFocusOrder(); + } + } + + private void updateFocusOrder() { + View prevView = null; + + // If the element that has focus becomes disabled or invisible, focus + // is given to the URL bar. + boolean needsNewFocus = false; + + for (View view : focusOrder) { + if (view.getVisibility() != View.VISIBLE || !view.isEnabled()) { + if (view.hasFocus()) { + needsNewFocus = true; + } + continue; + } + + if (view == actionItemBar) { + final int childCount = actionItemBar.getChildCount(); + for (int child = 0; child < childCount; child++) { + View childView = actionItemBar.getChildAt(child); + if (prevView != null) { + childView.setNextFocusLeftId(prevView.getId()); + prevView.setNextFocusRightId(childView.getId()); + } + prevView = childView; + } + } else { + if (prevView != null) { + view.setNextFocusLeftId(prevView.getId()); + prevView.setNextFocusRightId(view.getId()); + } + prevView = view; + } + } + + if (needsNewFocus) { + requestFocus(); + } + } + + public void onEditSuggestion(String suggestion) { + if (!isEditing()) { + return; + } + + urlEditLayout.onEditSuggestion(suggestion); + } + + public void setTitle(CharSequence title) { + urlDisplayLayout.setTitle(title); + } + + public void prepareTabsAnimation(PropertyAnimator animator, boolean tabsAreShown) { + if (!tabsAreShown) { + PropertyAnimator buttonsAnimator = + new PropertyAnimator(animator.getDuration(), buttonsInterpolator); + + buttonsAnimator.attach(tabsCounter, + PropertyAnimator.Property.ALPHA, + 1.0f); + + if (hasSoftMenuButton && !HardwareUtils.isTablet()) { + buttonsAnimator.attach(menuIcon, + PropertyAnimator.Property.ALPHA, + 1.0f); + } + + buttonsAnimator.start(); + + return; + } + + ViewHelper.setAlpha(tabsCounter, 0.0f); + + if (hasSoftMenuButton && !HardwareUtils.isTablet()) { + ViewHelper.setAlpha(menuIcon, 0.0f); + } + } + + public void finishTabsAnimation(boolean tabsAreShown) { + if (tabsAreShown) { + return; + } + + PropertyAnimator animator = new PropertyAnimator(150); + + animator.attach(tabsCounter, + PropertyAnimator.Property.ALPHA, + 1.0f); + + if (hasSoftMenuButton && !HardwareUtils.isTablet()) { + animator.attach(menuIcon, + PropertyAnimator.Property.ALPHA, + 1.0f); + } + + animator.start(); + } + + public void setOnActivateListener(OnActivateListener listener) { + activateListener = listener; + } + + public void setOnCommitListener(OnCommitListener listener) { + urlEditLayout.setOnCommitListener(listener); + } + + public void setOnDismissListener(OnDismissListener listener) { + urlEditLayout.setOnDismissListener(listener); + } + + public void setOnFilterListener(OnFilterListener listener) { + urlEditLayout.setOnFilterListener(listener); + } + + public void setOnFocusChangeListener(OnFocusChangeListener listener) { + focusChangeListener = listener; + } + + public void setOnStartEditingListener(OnStartEditingListener listener) { + startEditingListener = listener; + } + + public void setOnStopEditingListener(OnStopEditingListener listener) { + stopEditingListener = listener; + } + + private void showUrlEditLayout() { + setUrlEditLayoutVisibility(true, null); + } + + private void showUrlEditLayout(PropertyAnimator animator) { + setUrlEditLayoutVisibility(true, animator); + } + + private void hideUrlEditLayout() { + setUrlEditLayoutVisibility(false, null); + } + + private void hideUrlEditLayout(PropertyAnimator animator) { + setUrlEditLayoutVisibility(false, animator); + } + + private void setUrlEditLayoutVisibility(final boolean showEditLayout, PropertyAnimator animator) { + if (showEditLayout) { + urlEditLayout.prepareShowAnimation(animator); + } + + if (animator == null) { + final View viewToShow = (showEditLayout ? urlEditLayout : urlDisplayLayout); + final View viewToHide = (showEditLayout ? urlDisplayLayout : urlEditLayout); + + viewToHide.setVisibility(View.GONE); + viewToShow.setVisibility(View.VISIBLE); + + final int cancelVisibility = (showEditLayout ? View.VISIBLE : View.INVISIBLE); + setCancelVisibility(cancelVisibility); + return; + } + + animator.addPropertyAnimationListener(new PropertyAnimationListener() { + @Override + public void onPropertyAnimationStart() { + if (!showEditLayout) { + urlEditLayout.setVisibility(View.GONE); + urlDisplayLayout.setVisibility(View.VISIBLE); + + setCancelVisibility(View.INVISIBLE); + } + } + + @Override + public void onPropertyAnimationEnd() { + if (showEditLayout) { + urlDisplayLayout.setVisibility(View.GONE); + urlEditLayout.setVisibility(View.VISIBLE); + + setCancelVisibility(View.VISIBLE); + } + } + }); + } + + private void setCancelVisibility(final int visibility) { + if (editCancel != null) { + editCancel.setVisibility(visibility); + } + } + + /** + * Disables and dims all toolbar elements which are not + * related to editing mode. + */ + private void updateChildrenForEditing() { + // This is for the tablet UI only + if (!HardwareUtils.isTablet()) { + return; + } + + // Disable toolbar elemens while in editing mode + final boolean enabled = !isEditing(); + + // This alpha value has to be in sync with the one used + // in setButtonEnabled(). + final float alpha = (enabled ? 1.0f : 0.24f); + + if (!enabled) { + tabsCounter.onEnterEditingMode(); + } + + tabsButton.setEnabled(enabled); + ViewHelper.setAlpha(tabsCounter, alpha); + menuButton.setEnabled(enabled); + ViewHelper.setAlpha(menuIcon, alpha); + + final int actionItemsCount = actionItemBar.getChildCount(); + for (int i = 0; i < actionItemsCount; i++) { + actionItemBar.getChildAt(i).setEnabled(enabled); + } + ViewHelper.setAlpha(actionItemBar, alpha); + + final Tab tab = Tabs.getInstance().getSelectedTab(); + if (tab != null) { + setButtonEnabled(backButton, canDoBack(tab)); + setButtonEnabled(forwardButton, canDoForward(tab)); + + // Once the editing mode is finished, we have to ensure that the + // forward button slides away if necessary. This is because we might + // have only disabled it (without hiding it) when the toolbar entered + // editing mode. + if (!isEditing()) { + animateForwardButton(canDoForward(tab) ? + ForwardButtonAnimation.SHOW : ForwardButtonAnimation.HIDE); + } + } + } + + private void setUIMode(final UIMode uiMode) { + this.uiMode = uiMode; + urlEditLayout.setEnabled(uiMode == UIMode.EDIT); + } + + /** + * Returns whether or not the URL bar is in editing mode (url bar is expanded, hiding the new + * tab button). Note that selection state is independent of editing mode. + */ + public boolean isEditing() { + return (uiMode == UIMode.EDIT); + } + + public boolean isAnimating() { + return isAnimatingEntry; + } + + public void startEditing(String url, PropertyAnimator animator) { + if (isEditing()) { + return; + } + + urlEditLayout.setText(url != null ? url : ""); + + setUIMode(UIMode.EDIT); + updateChildrenForEditing(); + + updateProgressVisibility(); + + if (startEditingListener != null) { + startEditingListener.onStartEditing(); + } + + final int curveTranslation = getUrlBarCurveTranslation(); + final int entryTranslation = getUrlBarEntryTranslation(); + shouldShrinkURLBar = (entryTranslation < 0); + + if (urlBarTranslatingEdge != null) { + urlBarTranslatingEdge.setVisibility(View.VISIBLE); + if (shouldShrinkURLBar) { + urlBarEntry.setLayoutParams(urlBarEntryShrunkenLayoutParams); + } + } + + if (Build.VERSION.SDK_INT < 11) { + showEditingWithoutAnimation(entryTranslation, curveTranslation); + } else if (HardwareUtils.isTablet()) { + // No animation. + showUrlEditLayout(); + } else { + showEditingWithPhoneAnimation(animator, entryTranslation, curveTranslation); + } + } + + private void showEditingWithoutAnimation(final int entryTranslation, + final int curveTranslation) { + showUrlEditLayout(); + + if (urlBarTranslatingEdge != null) { + ViewHelper.setTranslationX(urlBarTranslatingEdge, entryTranslation); + } + + // Prevent taps through the editing mode cancel button (bug 1001243). + tabsButton.setEnabled(false); + + ViewHelper.setTranslationX(tabsButton, curveTranslation); + ViewHelper.setTranslationX(tabsCounter, curveTranslation); + ViewHelper.setTranslationX(actionItemBar, curveTranslation); + + if (hasSoftMenuButton) { + // Prevent tabs through the editing mode cancel button (bug 1001243). + menuButton.setEnabled(false); + + ViewHelper.setTranslationX(menuButton, curveTranslation); + ViewHelper.setTranslationX(menuIcon, curveTranslation); + } + } + + private void showEditingWithPhoneAnimation(final PropertyAnimator animator, + final int entryTranslation, final int curveTranslation) { + if (isAnimatingEntry) + return; + + urlDisplayLayout.prepareStartEditingAnimation(); + + // Slide toolbar elements. + if (urlBarTranslatingEdge != null) { + animator.attach(urlBarTranslatingEdge, + PropertyAnimator.Property.TRANSLATION_X, + entryTranslation); + } + + animator.attach(tabsButton, + PropertyAnimator.Property.TRANSLATION_X, + curveTranslation); + animator.attach(tabsCounter, + PropertyAnimator.Property.TRANSLATION_X, + curveTranslation); + animator.attach(actionItemBar, + PropertyAnimator.Property.TRANSLATION_X, + curveTranslation); + + if (hasSoftMenuButton) { + animator.attach(menuButton, + PropertyAnimator.Property.TRANSLATION_X, + curveTranslation); + + animator.attach(menuIcon, + PropertyAnimator.Property.TRANSLATION_X, + curveTranslation); + } + + showUrlEditLayout(animator); + + animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { + @Override + public void onPropertyAnimationStart() { + } + + @Override + public void onPropertyAnimationEnd() { + isAnimatingEntry = false; + } + }); + + isAnimatingEntry = true; + } + + /** + * Exits edit mode without updating the toolbar title. + * + * @return the url that was entered + */ + public String cancelEdit() { + Telemetry.stopUISession(TelemetryContract.Session.AWESOMESCREEN); + return stopEditing(); + } + + /** + * Exits edit mode, updating the toolbar title with the url that was just entered. + * + * @return the url that was entered + */ + public String commitEdit() { + final String url = stopEditing(); + if (!TextUtils.isEmpty(url)) { + setTitle(url); + } + return url; + } + + private String stopEditing() { + final String url = urlEditLayout.getText(); + if (!isEditing()) { + return url; + } + setUIMode(UIMode.DISPLAY); + + updateChildrenForEditing(); + + if (stopEditingListener != null) { + stopEditingListener.onStopEditing(); + } + + updateProgressVisibility(); + + // The animation looks cleaner if the text in the URL bar is + // not selected so clear the selection by clearing focus. + urlEditLayout.clearFocus(); + + if (Build.VERSION.SDK_INT < 11) { + stopEditingWithoutAnimation(); + } else if (HardwareUtils.isTablet()) { + // No animation. + hideUrlEditLayout(); + } else { + stopEditingWithPhoneAnimation(); + } + + return url; + } + + private void stopEditingWithoutAnimation() { + hideUrlEditLayout(); + + updateTabCountAndAnimate(Tabs.getInstance().getDisplayCount()); + + if (urlBarTranslatingEdge != null) { + urlBarTranslatingEdge.setVisibility(View.INVISIBLE); + ViewHelper.setTranslationX(urlBarTranslatingEdge, 0); + if (shouldShrinkURLBar) { + urlBarEntry.setLayoutParams(urlBarEntryDefaultLayoutParams); + } + } + + tabsButton.setEnabled(true); + + ViewHelper.setTranslationX(tabsButton, 0); + ViewHelper.setTranslationX(tabsCounter, 0); + ViewHelper.setTranslationX(actionItemBar, 0); + + if (hasSoftMenuButton) { + menuButton.setEnabled(true); + + ViewHelper.setTranslationX(menuButton, 0); + ViewHelper.setTranslationX(menuIcon, 0); + } + } + + private void stopEditingWithPhoneAnimation() { + final PropertyAnimator contentAnimator = new PropertyAnimator(250); + contentAnimator.setUseHardwareLayer(false); + + // Slide the toolbar back to its original size. + if (urlBarTranslatingEdge != null) { + contentAnimator.attach(urlBarTranslatingEdge, + PropertyAnimator.Property.TRANSLATION_X, + 0); + } + + contentAnimator.attach(tabsButton, + PropertyAnimator.Property.TRANSLATION_X, + 0); + contentAnimator.attach(tabsCounter, + PropertyAnimator.Property.TRANSLATION_X, + 0); + contentAnimator.attach(actionItemBar, + PropertyAnimator.Property.TRANSLATION_X, + 0); + + if (hasSoftMenuButton) { + contentAnimator.attach(menuButton, + PropertyAnimator.Property.TRANSLATION_X, + 0); + + contentAnimator.attach(menuIcon, + PropertyAnimator.Property.TRANSLATION_X, + 0); + } + + hideUrlEditLayout(contentAnimator); + + contentAnimator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { + @Override + public void onPropertyAnimationStart() { + } + + @Override + public void onPropertyAnimationEnd() { + if (urlBarTranslatingEdge != null) { + urlBarTranslatingEdge.setVisibility(View.INVISIBLE); + if (shouldShrinkURLBar) { + urlBarEntry.setLayoutParams(urlBarEntryDefaultLayoutParams); + } + } + + PropertyAnimator buttonsAnimator = new PropertyAnimator(300); + urlDisplayLayout.prepareStopEditingAnimation(buttonsAnimator); + buttonsAnimator.start(); + + isAnimatingEntry = false; + + // Trigger animation to update the tabs counter once the + // tabs button is back on screen. + updateTabCountAndAnimate(Tabs.getInstance().getDisplayCount()); + } + }); + + isAnimatingEntry = true; + contentAnimator.start(); + } + + private void setButtonEnabled(ImageButton button, boolean enabled) { + final Drawable drawable = button.getDrawable(); + if (drawable != null) { + // This alpha value has to be in sync with the one used + // in updateChildrenForEditing(). + drawable.setAlpha(enabled ? 255 : 61); + } + + button.setEnabled(enabled); + } + + public void updateBackButton(Tab tab) { + setButtonEnabled(backButton, canDoBack(tab)); + } + + private void animateForwardButton(final ForwardButtonAnimation animation) { + // If the forward button is not visible, we must be + // in the phone UI. + if (forwardButton.getVisibility() != View.VISIBLE) { + return; + } + + final boolean showing = (animation == ForwardButtonAnimation.SHOW); + + // if the forward button's margin is non-zero, this means it has already + // been animated to be visible¸ and vice-versa. + MarginLayoutParams fwdParams = (MarginLayoutParams) forwardButton.getLayoutParams(); + if ((fwdParams.leftMargin > defaultForwardMargin && showing) || + (fwdParams.leftMargin == defaultForwardMargin && !showing)) { + return; + } + + // We want the forward button to show immediately when switching tabs + final PropertyAnimator forwardAnim = + new PropertyAnimator(isSwitchingTabs ? 10 : FORWARD_ANIMATION_DURATION); + final int width = forwardButton.getWidth() / 2; + + forwardAnim.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { + @Override + public void onPropertyAnimationStart() { + if (!showing) { + // Set the margin before the transition when hiding the forward button. We + // have to do this so that the favicon isn't clipped during the transition + MarginLayoutParams layoutParams = + (MarginLayoutParams) urlDisplayLayout.getLayoutParams(); + layoutParams.leftMargin = 0; + + // Do the same on the URL edit container + layoutParams = (MarginLayoutParams) urlEditLayout.getLayoutParams(); + layoutParams.leftMargin = 0; + + requestLayout(); + // Note, we already translated the favicon, site security, and text field + // in prepareForwardAnimation, so they should appear to have not moved at + // all at this point. + } + } + + @Override + public void onPropertyAnimationEnd() { + if (showing) { + MarginLayoutParams layoutParams = + (MarginLayoutParams) urlDisplayLayout.getLayoutParams(); + layoutParams.leftMargin = urlBarViewOffset; + + layoutParams = (MarginLayoutParams) urlEditLayout.getLayoutParams(); + layoutParams.leftMargin = urlBarViewOffset; + } + + urlDisplayLayout.finishForwardAnimation(); + + MarginLayoutParams layoutParams = (MarginLayoutParams) forwardButton.getLayoutParams(); + layoutParams.leftMargin = defaultForwardMargin + (showing ? width : 0); + ViewHelper.setTranslationX(forwardButton, 0); + + requestLayout(); + } + }); + + prepareForwardAnimation(forwardAnim, animation, width); + forwardAnim.start(); + } + + public void updateForwardButton(Tab tab) { + final boolean enabled = canDoForward(tab); + if (forwardButton.isEnabled() == enabled) + return; + + // Save the state on the forward button so that we can skip animations + // when there's nothing to change + setButtonEnabled(forwardButton, enabled); + animateForwardButton(enabled ? ForwardButtonAnimation.SHOW : ForwardButtonAnimation.HIDE); + } + + private void prepareForwardAnimation(PropertyAnimator anim, ForwardButtonAnimation animation, int width) { + if (animation == ForwardButtonAnimation.HIDE) { + anim.attach(forwardButton, + PropertyAnimator.Property.TRANSLATION_X, + -width); + anim.attach(forwardButton, + PropertyAnimator.Property.ALPHA, + 0); + + } else { + anim.attach(forwardButton, + PropertyAnimator.Property.TRANSLATION_X, + width); + anim.attach(forwardButton, + PropertyAnimator.Property.ALPHA, + 1); + } + + urlDisplayLayout.prepareForwardAnimation(anim, animation, width); + } + + @Override + public boolean addActionItem(View actionItem) { + actionItemBar.addView(actionItem); + return true; + } + + @Override + public void removeActionItem(View actionItem) { + actionItemBar.removeView(actionItem); + } + + @Override + public void setPrivateMode(boolean isPrivate) { + super.setPrivateMode(isPrivate); + + tabsButton.setPrivateMode(isPrivate); + menuButton.setPrivateMode(isPrivate); + menuIcon.setPrivateMode(isPrivate); + urlEditLayout.setPrivateMode(isPrivate); + + if (backButton instanceof BackButton) { + ((BackButton) backButton).setPrivateMode(isPrivate); + } + + if (forwardButton instanceof ForwardButton) { + ((ForwardButton) forwardButton).setPrivateMode(isPrivate); + } + } + + public void show() { + setVisibility(View.VISIBLE); + } + + public void hide() { + setVisibility(View.GONE); + } + + public View getDoorHangerAnchor() { + return urlDisplayLayout.getDoorHangerAnchor(); + } + + public void onDestroy() { + Tabs.unregisterOnTabsChangedListener(this); + + unregisterEventListener("Reader:Click"); + unregisterEventListener("Reader:LongClick"); + } + + public boolean openOptionsMenu() { + if (!hasSoftMenuButton) { + return false; + } + + // Initialize the popup. + if (menuPopup == null) { + View panel = activity.getMenuPanel(); + menuPopup = new MenuPopup(activity); + menuPopup.setPanelView(panel); + + menuPopup.setOnDismissListener(new PopupWindow.OnDismissListener() { + @Override + public void onDismiss() { + activity.onOptionsMenuClosed(null); + } + }); + } + + GeckoAppShell.getGeckoInterface().invalidateOptionsMenu(); + if (!menuPopup.isShowing()) { + menuPopup.showAsDropDown(menuButton); + } + + return true; + } + + public boolean closeOptionsMenu() { + if (!hasSoftMenuButton) { + return false; + } + + if (menuPopup != null && menuPopup.isShowing()) { + menuPopup.dismiss(); + } + + return true; + } + + private void registerEventListener(String event) { + GeckoAppShell.getEventDispatcher().registerEventListener(event, this); + } + + private void unregisterEventListener(String event) { + GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this); + } + + @Override + public void handleMessage(String event, JSONObject message) { + Log.d(LOGTAG, "handleMessage: " + event); + if (event.equals("Reader:Click")) { + Tab tab = Tabs.getInstance().getSelectedTab(); + if (tab != null) { + tab.toggleReaderMode(); + } + } else if (event.equals("Reader:LongClick")) { + Tab tab = Tabs.getInstance().getSelectedTab(); + if (tab != null) { + tab.addToReadingList(); + } + } + } + + @Override + public void onLightweightThemeChanged() { + Drawable drawable = theme.getDrawable(this); + if (drawable == null) + return; + + StateListDrawable stateList = new StateListDrawable(); + stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.background_private)); + stateList.addState(EMPTY_STATE_SET, drawable); + + setBackgroundDrawable(stateList); + + if (editCancel != null) { + editCancel.onLightweightThemeChanged(); + } + } + + @Override + public void onLightweightThemeReset() { + setBackgroundResource(R.drawable.url_bar_bg); + if (editCancel != null) { + editCancel.onLightweightThemeReset(); + } + } +}