import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -0,0 +1,10 @@
/* -*- 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;
public interface AutocompleteHandler {
void onAutocomplete(String res);
}

View file

@ -0,0 +1,26 @@
/* 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 android.content.Context;
import android.graphics.Path;
import android.util.AttributeSet;
public class BackButton extends NavButton {
public BackButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
mPath.reset();
mPath.addCircle(width / 2, height / 2, width / 2, Path.Direction.CW);
mBorderPath.reset();
mBorderPath.addCircle(width / 2, height / 2, (width / 2) - (mBorderWidth / 2), Path.Direction.CW);
}
}

View file

@ -0,0 +1,960 @@
/* -*- 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.EnumSet;
import java.util.List;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.R;
import org.mozilla.gecko.SiteIdentity;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.TouchEventInterceptor;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.lwt.LightweightTheme;
import org.mozilla.gecko.lwt.LightweightThemeDrawable;
import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.menu.MenuPopup;
import org.mozilla.gecko.tabs.TabHistoryController;
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.HardwareUtils;
import org.mozilla.gecko.util.MenuUtils;
import org.mozilla.gecko.widget.themed.ThemedFrameLayout;
import org.mozilla.gecko.widget.themed.ThemedImageButton;
import org.mozilla.gecko.widget.themed.ThemedImageView;
import org.mozilla.gecko.widget.themed.ThemedRelativeLayout;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.support.annotation.NonNull;
/**
* {@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 abstract class BrowserToolbar extends ThemedRelativeLayout
implements Tabs.OnTabsChangedListener,
GeckoMenu.ActionItemBarPresenter {
private static final String LOGTAG = "GeckoToolbar";
private static final int LIGHTWEIGHT_THEME_INVERT_ALPHA = 34; // 255 - alpha = invert_alpha
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();
}
protected enum UIMode {
EDIT,
DISPLAY
}
protected final ToolbarDisplayLayout urlDisplayLayout;
protected final ToolbarEditLayout urlEditLayout;
protected final View urlBarEntry;
protected boolean isSwitchingTabs;
protected final ThemedImageButton tabsButton;
private ToolbarProgressView progressBar;
protected final TabCounter tabsCounter;
protected final ThemedFrameLayout menuButton;
protected final ThemedImageView menuIcon;
private MenuPopup menuPopup;
protected final List<View> focusOrder;
private OnActivateListener activateListener;
private OnFocusChangeListener focusChangeListener;
private OnStartEditingListener startEditingListener;
private OnStopEditingListener stopEditingListener;
private TouchEventInterceptor mTouchEventInterceptor;
protected final BrowserApp activity;
protected UIMode uiMode;
protected TabHistoryController tabHistoryController;
private final Paint shadowPaint;
private final int shadowColor;
private final int shadowPrivateColor;
private final int shadowSize;
private final ToolbarPrefs prefs;
public abstract boolean isAnimating();
protected abstract boolean isTabsButtonOffscreen();
protected abstract void updateNavigationButtons(Tab tab);
protected abstract void triggerStartEditingTransition(PropertyAnimator animator);
protected abstract void triggerStopEditingTransition();
public abstract void triggerTabsPanelTransition(PropertyAnimator animator, boolean areTabsShown);
/**
* Returns a Drawable overlaid with the theme's bitmap.
*/
protected Drawable getLWTDefaultStateSetDrawable() {
return getTheme().getDrawable(this);
}
public static BrowserToolbar create(final Context context, final AttributeSet attrs) {
final boolean isLargeResource = context.getResources().getBoolean(R.bool.is_large_resource);
final BrowserToolbar toolbar;
if (isLargeResource) {
toolbar = new BrowserToolbarTablet(context, attrs);
} else {
toolbar = new BrowserToolbarPhone(context, attrs);
}
return toolbar;
}
protected BrowserToolbar(final Context context, final AttributeSet attrs) {
super(context, attrs);
setWillNotDraw(false);
// BrowserToolbar is attached to BrowserApp only.
activity = (BrowserApp) context;
LayoutInflater.from(context).inflate(R.layout.browser_toolbar, this);
Tabs.registerOnTabsChangedListener(this);
isSwitchingTabs = true;
urlDisplayLayout = (ToolbarDisplayLayout) findViewById(R.id.display_layout);
urlBarEntry = findViewById(R.id.url_bar_entry);
urlEditLayout = (ToolbarEditLayout) findViewById(R.id.edit_layout);
tabsButton = (ThemedImageButton) findViewById(R.id.tabs);
tabsCounter = (TabCounter) findViewById(R.id.tabs_counter);
tabsCounter.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
menuButton = (ThemedFrameLayout) findViewById(R.id.menu);
menuIcon = (ThemedImageView) findViewById(R.id.menu_icon);
// The focusOrder List should be filled by sub-classes.
focusOrder = new ArrayList<View>();
final Resources res = getResources();
shadowSize = res.getDimensionPixelSize(R.dimen.browser_toolbar_shadow_size);
shadowPaint = new Paint();
shadowColor = ContextCompat.getColor(context, R.color.url_bar_shadow);
shadowPrivateColor = ContextCompat.getColor(context, R.color.url_bar_shadow_private);
shadowPaint.setColor(shadowColor);
shadowPaint.setStrokeWidth(0.0f);
setUIMode(UIMode.DISPLAY);
prefs = new ToolbarPrefs();
urlDisplayLayout.setToolbarPrefs(prefs);
urlEditLayout.setToolbarPrefs(prefs);
setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() {
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
// Do not show 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.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.subscribe, false);
MenuUtils.safeSetVisible(menu, R.id.add_search_engine, false);
}
}
});
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (activateListener != null) {
activateListener.onActivate();
}
}
});
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
prefs.open();
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) {
// Clear focus so a back press with the tabs
// panel open does not go to the editing field.
urlEditLayout.clearFocus();
toggleTabs();
}
});
tabsButton.setImageLevel(0);
menuButton.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View view) {
// Drop the soft keyboard.
urlEditLayout.clearFocus();
activity.openOptionsMenu();
}
});
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
prefs.close();
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
final int height = getHeight();
canvas.drawRect(0, height - shadowSize, getWidth(), height, shadowPaint);
}
public void onParentFocus() {
urlEditLayout.onParentFocus();
}
public void setProgressBar(ToolbarProgressView progressBar) {
this.progressBar = progressBar;
}
public void setTabHistoryController(TabHistoryController tabHistoryController) {
this.tabHistoryController = tabHistoryController;
}
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() && !isAnimating()) {
Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL,
TelemetryContract.Method.BACK);
cancelEdit();
return true;
}
return urlDisplayLayout.dismissSiteIdentityPopup();
}
@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();
}
});
}
}
public void saveTabEditingState(final TabEditingState editingState) {
urlEditLayout.saveTabEditingState(editingState);
}
public void restoreTabEditingState(final TabEditingState editingState) {
if (!isEditing()) {
throw new IllegalStateException("Expected to be editing");
}
urlEditLayout.restoreTabEditingState(editingState);
}
@Override
public void onTabChanged(@Nullable Tab tab, Tabs.TabEvents msg, String 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<UpdateFlags> 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:
updateNavigationButtons(tab);
break;
case SELECTED:
flags.add(UpdateFlags.PRIVATE_MODE);
setPrivateMode(tab.isPrivate());
// Fall through.
case LOAD_ERROR:
case LOCATION_CHANGE:
// We're displaying the tab URL in place of the title,
// so we always need to update our "title" here as well.
flags.add(UpdateFlags.TITLE);
flags.add(UpdateFlags.FAVICON);
flags.add(UpdateFlags.SITE_IDENTITY);
updateNavigationButtons(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() && tab != null) {
updateDisplayLayout(tab, flags);
}
}
switch (msg) {
case SELECTED:
case LOAD_ERROR:
case LOCATION_CHANGE:
isSwitchingTabs = false;
}
}
private void updateProgressVisibility() {
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
// The selected tab may be null if GeckoApp (and thus the
// selected tab) are not yet initialized (bug 1090287).
if (selectedTab != null) {
updateProgressVisibility(selectedTab, selectedTab.getLoadProgress());
}
}
private void updateProgressVisibility(Tab selectedTab, int progress) {
if (!isEditing() && selectedTab.getState() == Tab.STATE_LOADING) {
progressBar.setProgress(progress);
progressBar.setPrivateMode(selectedTab.isPrivate());
progressBar.setVisibility(View.VISIBLE);
} else {
progressBar.setVisibility(View.GONE);
}
}
protected boolean isVisible() {
return ViewHelper.getTranslationY(this) == 0;
}
@Override
public void setNextFocusDownId(int nextId) {
super.setNextFocusDownId(nextId);
tabsButton.setNextFocusDownId(nextId);
urlDisplayLayout.setNextFocusDownId(nextId);
menuButton.setNextFocusDownId(nextId);
}
public boolean hideVirtualKeyboard() {
InputMethodManager imm =
(InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
return imm.hideSoftInputFromWindow(tabsButton.getWindowToken(), 0);
}
private void showSelectedTabs() {
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
if (!tab.isPrivate())
activity.showNormalTabs();
else
activity.showPrivateTabs();
}
}
private void toggleTabs() {
if (activity.areTabsShown()) {
return;
}
if (hideVirtualKeyboard()) {
getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
getViewTreeObserver().removeGlobalOnLayoutListener(this);
showSelectedTabs();
}
});
} else {
showSelectedTabs();
}
}
protected void updateTabCount(final 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 (isTabsButtonOffscreen()) {
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(@NonNull Tab tab, EnumSet<UpdateFlags> 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() {
if (focusOrder.size() == 0) {
throw new IllegalStateException("Expected focusOrder to be initialized in subclass");
}
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.getId() == R.id.menu_items) {
final LinearLayout actionItemBar = (LinearLayout) view;
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 setToolBarButtonsAlpha(float alpha) {
ViewHelper.setAlpha(tabsCounter, alpha);
if (!HardwareUtils.isTablet()) {
ViewHelper.setAlpha(menuIcon, alpha);
}
}
public void onEditSuggestion(String suggestion) {
if (!isEditing()) {
return;
}
urlEditLayout.onEditSuggestion(suggestion);
}
public void setTitle(CharSequence title) {
urlDisplayLayout.setTitle(title);
}
public void setOnActivateListener(final 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);
}
@Override
public void setOnFocusChangeListener(OnFocusChangeListener listener) {
focusChangeListener = listener;
}
public void setOnStartEditingListener(OnStartEditingListener listener) {
startEditingListener = listener;
}
public void setOnStopEditingListener(OnStopEditingListener listener) {
stopEditingListener = listener;
}
protected void showUrlEditLayout() {
setUrlEditLayoutVisibility(true, null);
}
protected void showUrlEditLayout(final PropertyAnimator animator) {
setUrlEditLayoutVisibility(true, animator);
}
protected void hideUrlEditLayout() {
setUrlEditLayoutVisibility(false, null);
}
protected void hideUrlEditLayout(final PropertyAnimator animator) {
setUrlEditLayoutVisibility(false, animator);
}
protected void setUrlEditLayoutVisibility(final boolean showEditLayout, PropertyAnimator animator) {
if (showEditLayout) {
urlEditLayout.prepareShowAnimation(animator);
}
// If this view is GONE, we trigger a measure pass when setting the view to
// VISIBLE. Since this will occur during the toolbar open animation, it causes jank.
final int hiddenViewVisibility = View.INVISIBLE;
if (animator == null) {
final View viewToShow = (showEditLayout ? urlEditLayout : urlDisplayLayout);
final View viewToHide = (showEditLayout ? urlDisplayLayout : urlEditLayout);
viewToHide.setVisibility(hiddenViewVisibility);
viewToShow.setVisibility(View.VISIBLE);
return;
}
animator.addPropertyAnimationListener(new PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() {
if (!showEditLayout) {
urlEditLayout.setVisibility(hiddenViewVisibility);
urlDisplayLayout.setVisibility(View.VISIBLE);
}
}
@Override
public void onPropertyAnimationEnd() {
if (showEditLayout) {
urlDisplayLayout.setVisibility(hiddenViewVisibility);
urlEditLayout.setVisibility(View.VISIBLE);
}
}
});
}
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 void startEditing(String url, PropertyAnimator animator) {
if (isEditing()) {
return;
}
urlEditLayout.setText(url != null ? url : "");
setUIMode(UIMode.EDIT);
updateProgressVisibility();
if (startEditingListener != null) {
startEditingListener.onStartEditing();
}
triggerStartEditingTransition(animator);
}
/**
* 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() {
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
tab.resetSiteIdentity();
}
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);
if (stopEditingListener != null) {
stopEditingListener.onStopEditing();
}
updateProgressVisibility();
triggerStopEditingTransition();
return url;
}
@Override
public void setPrivateMode(boolean isPrivate) {
super.setPrivateMode(isPrivate);
tabsButton.setPrivateMode(isPrivate);
menuButton.setPrivateMode(isPrivate);
urlEditLayout.setPrivateMode(isPrivate);
shadowPaint.setColor(isPrivate ? shadowPrivateColor : shadowColor);
}
public void show() {
setVisibility(View.VISIBLE);
}
public void hide() {
setVisibility(View.GONE);
}
public View getDoorHangerAnchor() {
return urlDisplayLayout;
}
public void onDestroy() {
Tabs.unregisterOnTabsChangedListener(this);
urlDisplayLayout.destroy();
}
public boolean openOptionsMenu() {
// 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 (menuPopup != null && menuPopup.isShowing()) {
menuPopup.dismiss();
}
return true;
}
@Override
public void onLightweightThemeChanged() {
final Drawable drawable = getLWTDefaultStateSetDrawable();
if (drawable == null) {
return;
}
final StateListDrawable stateList = new StateListDrawable();
stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.tabs_tray_grey_pressed));
stateList.addState(EMPTY_STATE_SET, drawable);
setBackgroundDrawable(stateList);
}
public void setTouchEventInterceptor(TouchEventInterceptor interceptor) {
mTouchEventInterceptor = interceptor;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (mTouchEventInterceptor != null && mTouchEventInterceptor.onInterceptTouchEvent(this, event)) {
return true;
}
return super.onInterceptTouchEvent(event);
}
@Override
public void onLightweightThemeReset() {
setBackgroundResource(R.drawable.url_bar_bg);
}
public static LightweightThemeDrawable getLightweightThemeDrawable(final View view,
final LightweightTheme theme, final int colorResID) {
final int color = ContextCompat.getColor(view.getContext(), colorResID);
final LightweightThemeDrawable drawable = theme.getColorDrawable(view, color);
if (drawable != null) {
drawable.setAlpha(LIGHTWEIGHT_THEME_INVERT_ALPHA, LIGHTWEIGHT_THEME_INVERT_ALPHA);
}
return drawable;
}
public static class TabEditingState {
// The edited text from the most recent time this tab was unselected.
protected String lastEditingText;
protected int selectionStart;
protected int selectionEnd;
public boolean isBrowserSearchShown;
public void copyFrom(final TabEditingState s2) {
lastEditingText = s2.lastEditingText;
selectionStart = s2.selectionStart;
selectionEnd = s2.selectionEnd;
isBrowserSearchShown = s2.isBrowserSearchShown;
}
public boolean isBrowserSearchShown() {
return isBrowserSearchShown;
}
public void setIsBrowserSearchShown(final boolean isShown) {
isBrowserSearchShown = isShown;
}
}
}

View file

@ -0,0 +1,128 @@
/* -*- 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 org.mozilla.gecko.Tabs;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener;
import org.mozilla.gecko.util.HardwareUtils;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
/**
* A toolbar implementation for phones.
*/
class BrowserToolbarPhone extends BrowserToolbarPhoneBase {
private final PropertyAnimationListener showEditingListener;
private final PropertyAnimationListener stopEditingListener;
protected boolean isAnimatingEntry;
protected BrowserToolbarPhone(final Context context, final AttributeSet attrs) {
super(context, attrs);
// Create these listeners here, once, to avoid constructing new listeners
// each time they are set on an animator (i.e. each time the url bar is clicked).
showEditingListener = new PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() { /* Do nothing */ }
@Override
public void onPropertyAnimationEnd() {
isAnimatingEntry = false;
}
};
stopEditingListener = new PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() { /* Do nothing */ }
@Override
public void onPropertyAnimationEnd() {
urlBarTranslatingEdge.setVisibility(View.INVISIBLE);
final 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());
}
};
}
@Override
public boolean isAnimating() {
return isAnimatingEntry;
}
@Override
protected void triggerStartEditingTransition(final PropertyAnimator animator) {
if (isAnimatingEntry) {
return;
}
// The animation looks cleaner if the text in the URL bar is
// not selected so clear the selection by clearing focus.
urlEditLayout.clearFocus();
urlDisplayLayout.prepareStartEditingAnimation();
addAnimationsForEditing(animator, true);
showUrlEditLayout(animator);
urlBarTranslatingEdge.setVisibility(View.VISIBLE);
animator.addPropertyAnimationListener(showEditingListener);
isAnimatingEntry = true; // To be correct, this should be called last.
}
@Override
protected void triggerStopEditingTransition() {
final PropertyAnimator animator = new PropertyAnimator(250);
animator.setUseHardwareLayer(false);
addAnimationsForEditing(animator, false);
hideUrlEditLayout(animator);
animator.addPropertyAnimationListener(stopEditingListener);
isAnimatingEntry = true;
animator.start();
}
private void addAnimationsForEditing(final PropertyAnimator animator, final boolean isEditing) {
final int curveTranslation;
final int entryTranslation;
if (isEditing) {
curveTranslation = getUrlBarCurveTranslation();
entryTranslation = getUrlBarEntryTranslation();
} else {
curveTranslation = 0;
entryTranslation = 0;
}
// Slide toolbar elements.
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(menuButton,
PropertyAnimator.Property.TRANSLATION_X,
curveTranslation);
animator.attach(menuIcon,
PropertyAnimator.Property.TRANSLATION_X,
curveTranslation);
}
}

View file

@ -0,0 +1,219 @@
/* -*- 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.Arrays;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.widget.themed.ThemedImageView;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.ImageView;
/**
* A base implementations of the browser toolbar for phones.
* This class manages any Views, variables, etc. that are exclusive to phone.
*/
abstract class BrowserToolbarPhoneBase extends BrowserToolbar {
protected final ImageView urlBarTranslatingEdge;
protected final ThemedImageView editCancel;
private final Path roundCornerShape;
private final Paint roundCornerPaint;
private final Interpolator buttonsInterpolator = new AccelerateInterpolator();
public BrowserToolbarPhoneBase(final Context context, final AttributeSet attrs) {
super(context, attrs);
final Resources res = context.getResources();
urlBarTranslatingEdge = (ImageView) findViewById(R.id.url_bar_translating_edge);
// This will clip the translating edge's image at 60% of its width
urlBarTranslatingEdge.getDrawable().setLevel(6000);
editCancel = (ThemedImageView) findViewById(R.id.edit_cancel);
focusOrder.add(this);
focusOrder.addAll(urlDisplayLayout.getFocusOrder());
focusOrder.addAll(Arrays.asList(tabsButton, menuButton));
roundCornerShape = new Path();
roundCornerShape.moveTo(0, 0);
roundCornerShape.lineTo(30, 0);
roundCornerShape.cubicTo(0, 0, 0, 0, 0, 30);
roundCornerShape.lineTo(0, 0);
roundCornerPaint = new Paint();
roundCornerPaint.setAntiAlias(true);
roundCornerPaint.setColor(ContextCompat.getColor(context, R.color.text_and_tabs_tray_grey));
roundCornerPaint.setStrokeWidth(0.0f);
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
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 (!isAnimating()) {
Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL,
TelemetryContract.Method.ACTIONBAR,
getResources().getResourceEntryName(editCancel.getId()));
cancelEdit();
}
}
});
}
@Override
public void setPrivateMode(final boolean isPrivate) {
super.setPrivateMode(isPrivate);
editCancel.setPrivateMode(isPrivate);
}
@Override
protected boolean isTabsButtonOffscreen() {
return isEditing();
}
@Override
public boolean addActionItem(final View actionItem) {
// We have no action item bar.
return false;
}
@Override
public void removeActionItem(final View actionItem) {
// We have no action item bar.
}
@Override
protected void updateNavigationButtons(final Tab tab) {
// We have no navigation buttons so do nothing.
}
@Override
public void draw(final Canvas canvas) {
super.draw(canvas);
if (uiMode == UIMode.DISPLAY) {
canvas.drawPath(roundCornerShape, roundCornerPaint);
}
}
@Override
public void triggerTabsPanelTransition(final PropertyAnimator animator, final boolean areTabsShown) {
if (areTabsShown) {
ViewHelper.setAlpha(tabsCounter, 0.0f);
ViewHelper.setAlpha(menuIcon, 0.0f);
return;
}
final PropertyAnimator buttonsAnimator =
new PropertyAnimator(animator.getDuration(), buttonsInterpolator);
buttonsAnimator.attach(tabsCounter,
PropertyAnimator.Property.ALPHA,
1.0f);
buttonsAnimator.attach(menuIcon,
PropertyAnimator.Property.ALPHA,
1.0f);
buttonsAnimator.start();
}
/**
* Returns the number of pixels the url bar translating edge
* needs to translate to the right to enter its editing mode state.
* A negative value means the edge must translate to the left.
*/
protected int getUrlBarEntryTranslation() {
// 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).
return editCancel.getLeft() - urlBarEntry.getRight();
}
protected int getUrlBarCurveTranslation() {
return getWidth() - tabsButton.getLeft();
}
protected void updateTabCountAndAnimate(final 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 (!isTabsButtonOffscreen()) {
tabsCounter.setCount(count);
tabsButton.setContentDescription((count > 1) ?
activity.getString(R.string.num_tabs, count) :
activity.getString(R.string.one_tab));
}
}
@Override
protected void setUrlEditLayoutVisibility(final boolean showEditLayout,
final PropertyAnimator animator) {
super.setUrlEditLayoutVisibility(showEditLayout, animator);
if (animator == null) {
editCancel.setVisibility(showEditLayout ? View.VISIBLE : View.INVISIBLE);
return;
}
animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() {
if (!showEditLayout) {
editCancel.setVisibility(View.INVISIBLE);
}
}
@Override
public void onPropertyAnimationEnd() {
if (showEditLayout) {
editCancel.setVisibility(View.VISIBLE);
}
}
});
}
@Override
public void onLightweightThemeChanged() {
super.onLightweightThemeChanged();
editCancel.onLightweightThemeChanged();
}
@Override
public void onLightweightThemeReset() {
super.onLightweightThemeReset();
editCancel.onLightweightThemeReset();
}
}

View file

@ -0,0 +1,211 @@
/* -*- 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 org.mozilla.gecko.R;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.ViewHelper;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
/**
* The toolbar implementation for tablet.
*/
class BrowserToolbarTablet extends BrowserToolbarTabletBase {
private static final int FORWARD_ANIMATION_DURATION = 450;
private enum ForwardButtonState {
HIDDEN,
DISPLAYED,
TRANSITIONING,
}
private final int forwardButtonTranslationWidth;
private ForwardButtonState forwardButtonState;
private boolean backButtonWasEnabledOnStartEditing;
public BrowserToolbarTablet(final Context context, final AttributeSet attrs) {
super(context, attrs);
forwardButtonTranslationWidth =
getResources().getDimensionPixelOffset(R.dimen.tablet_nav_button_width);
// The forward button is initially expanded (in the layout file)
// so translate it for start of the expansion animation; future
// iterations translate it to this position when hiding and will already be set up.
ViewHelper.setTranslationX(forwardButton, -forwardButtonTranslationWidth);
// TODO: Move this to *TabletBase when old tablet is removed.
// We don't want users clicking the forward button in transitions, but we don't want it to
// look disabled to avoid flickering complications (e.g. disabled in editing mode), so undo
// the work of the super class' constructor.
forwardButton.setEnabled(true);
updateForwardButtonState(ForwardButtonState.HIDDEN);
}
private void updateForwardButtonState(final ForwardButtonState state) {
forwardButtonState = state;
forwardButton.setEnabled(forwardButtonState == ForwardButtonState.DISPLAYED);
}
@Override
public boolean isAnimating() {
return false;
}
@Override
protected void triggerStartEditingTransition(final PropertyAnimator animator) {
showUrlEditLayout();
}
@Override
protected void triggerStopEditingTransition() {
hideUrlEditLayout();
}
@Override
protected void animateForwardButton(final ForwardButtonAnimation animation) {
final boolean willShowForward = (animation == ForwardButtonAnimation.SHOW);
if ((forwardButtonState != ForwardButtonState.HIDDEN && willShowForward) ||
(forwardButtonState != ForwardButtonState.DISPLAYED && !willShowForward)) {
return;
}
updateForwardButtonState(ForwardButtonState.TRANSITIONING);
// We want the forward button to show immediately when switching tabs
final PropertyAnimator forwardAnim =
new PropertyAnimator(isSwitchingTabs ? 10 : FORWARD_ANIMATION_DURATION);
forwardAnim.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() {
if (!willShowForward) {
// 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() {
final ForwardButtonState newForwardButtonState;
if (willShowForward) {
// Increase the margins to ensure the text does not run outside the View.
MarginLayoutParams layoutParams =
(MarginLayoutParams) urlDisplayLayout.getLayoutParams();
layoutParams.leftMargin = forwardButtonTranslationWidth;
layoutParams = (MarginLayoutParams) urlEditLayout.getLayoutParams();
layoutParams.leftMargin = forwardButtonTranslationWidth;
newForwardButtonState = ForwardButtonState.DISPLAYED;
} else {
newForwardButtonState = ForwardButtonState.HIDDEN;
}
urlDisplayLayout.finishForwardAnimation();
updateForwardButtonState(newForwardButtonState);
requestLayout();
}
});
prepareForwardAnimation(forwardAnim, animation, forwardButtonTranslationWidth);
forwardAnim.start();
}
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,
0);
anim.attach(forwardButton,
PropertyAnimator.Property.ALPHA,
1);
}
urlDisplayLayout.prepareForwardAnimation(anim, animation, width);
}
@Override
public void triggerTabsPanelTransition(final PropertyAnimator animator, final boolean areTabsShown) {
// Do nothing.
}
@Override
public void setToolBarButtonsAlpha(float alpha) {
// Do nothing.
}
@Override
public void startEditing(final String url, final PropertyAnimator animator) {
// We already know the forward button state - no need to store it here.
backButtonWasEnabledOnStartEditing = backButton.isEnabled();
backButton.setEnabled(false);
forwardButton.setEnabled(false);
super.startEditing(url, animator);
}
@Override
public String commitEdit() {
stopEditingNewTablet();
return super.commitEdit();
}
@Override
public String cancelEdit() {
// This can get called when we're not editing but we only want
// to make these changes when leaving editing mode.
if (isEditing()) {
stopEditingNewTablet();
backButton.setEnabled(backButtonWasEnabledOnStartEditing);
updateForwardButtonState(forwardButtonState);
}
return super.cancelEdit();
}
private void stopEditingNewTablet() {
// Undo the changes caused by calling setEnabled for forwardButton in startEditing.
// Note that this should be called first so the enabled state of the
// forward button is set to the proper value.
forwardButton.setEnabled(true);
}
@Override
protected Drawable getLWTDefaultStateSetDrawable() {
return BrowserToolbar.getLightweightThemeDrawable(this, getTheme(), R.color.toolbar_grey);
}
}

View file

@ -0,0 +1,182 @@
/* -*- 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.Arrays;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.tabs.TabHistoryController;
import org.mozilla.gecko.menu.MenuItemActionBar;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.widget.themed.ThemedTextView;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.LinearLayout;
/**
* A base implementations of the browser toolbar for tablets.
* This class manages any Views, variables, etc. that are exclusive to tablet.
*/
abstract class BrowserToolbarTabletBase extends BrowserToolbar {
protected enum ForwardButtonAnimation {
SHOW,
HIDE
}
protected final LinearLayout actionItemBar;
protected final BackButton backButton;
protected final ForwardButton forwardButton;
protected final View menuButtonMarginView;
private final PorterDuffColorFilter privateBrowsingTabletMenuItemColorFilter;
protected abstract void animateForwardButton(ForwardButtonAnimation animation);
public BrowserToolbarTabletBase(final Context context, final AttributeSet attrs) {
super(context, attrs);
actionItemBar = (LinearLayout) findViewById(R.id.menu_items);
backButton = (BackButton) findViewById(R.id.back);
backButton.setEnabled(false);
forwardButton = (ForwardButton) findViewById(R.id.forward);
forwardButton.setEnabled(false);
initButtonListeners();
focusOrder.addAll(Arrays.asList(tabsButton, (View) backButton, (View) forwardButton, this));
focusOrder.addAll(urlDisplayLayout.getFocusOrder());
focusOrder.addAll(Arrays.asList(actionItemBar, menuButton));
urlDisplayLayout.updateSiteIdentityAnchor(backButton);
privateBrowsingTabletMenuItemColorFilter = new PorterDuffColorFilter(
ContextCompat.getColor(context, R.color.tabs_tray_icon_grey), PorterDuff.Mode.SRC_IN);
menuButtonMarginView = findViewById(R.id.menu_margin);
if (menuButtonMarginView != null) {
menuButtonMarginView.setVisibility(View.VISIBLE);
}
}
private void initButtonListeners() {
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 tabHistoryController.showTabHistory(Tabs.getInstance().getSelectedTab(),
TabHistoryController.HistoryAction.BACK);
}
});
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 tabHistoryController.showTabHistory(Tabs.getInstance().getSelectedTab(),
TabHistoryController.HistoryAction.FORWARD);
}
});
}
@Override
protected boolean isTabsButtonOffscreen() {
return false;
}
@Override
public boolean addActionItem(final View actionItem) {
actionItemBar.addView(actionItem, LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
return true;
}
@Override
public void removeActionItem(final View actionItem) {
actionItemBar.removeView(actionItem);
}
@Override
protected void updateNavigationButtons(final Tab tab) {
backButton.setEnabled(canDoBack(tab));
animateForwardButton(
canDoForward(tab) ? ForwardButtonAnimation.SHOW : ForwardButtonAnimation.HIDE);
}
@Override
public void setNextFocusDownId(int nextId) {
super.setNextFocusDownId(nextId);
backButton.setNextFocusDownId(nextId);
forwardButton.setNextFocusDownId(nextId);
}
@Override
public void setPrivateMode(final boolean isPrivate) {
super.setPrivateMode(isPrivate);
// If we had backgroundTintList, we could remove the colorFilter
// code in favor of setPrivateMode (bug 1197432).
final PorterDuffColorFilter colorFilter =
isPrivate ? privateBrowsingTabletMenuItemColorFilter : null;
setTabsCounterPrivateMode(isPrivate, colorFilter);
backButton.setPrivateMode(isPrivate);
forwardButton.setPrivateMode(isPrivate);
menuIcon.setPrivateMode(isPrivate);
for (int i = 0; i < actionItemBar.getChildCount(); ++i) {
final MenuItemActionBar child = (MenuItemActionBar) actionItemBar.getChildAt(i);
child.setPrivateMode(isPrivate);
}
}
private void setTabsCounterPrivateMode(final boolean isPrivate, final PorterDuffColorFilter colorFilter) {
// The TabsCounter is a TextSwitcher which cycles two views
// to provide animations, hence looping over these two children.
for (int i = 0; i < 2; ++i) {
final ThemedTextView view = (ThemedTextView) tabsCounter.getChildAt(i);
view.setPrivateMode(isPrivate);
view.getBackground().mutate().setColorFilter(colorFilter);
}
// To prevent animation of the background,
// it is set to a different Drawable.
tabsCounter.getBackground().mutate().setColorFilter(colorFilter);
}
@Override
public View getDoorHangerAnchor() {
return backButton;
}
protected boolean canDoBack(final Tab tab) {
return (tab.canDoBack() && !isEditing());
}
protected boolean canDoForward(final Tab tab) {
return (tab.canDoForward() && !isEditing());
}
}

View file

@ -0,0 +1,62 @@
/* 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 org.mozilla.gecko.AppConstants.Versions;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffXfermode;
import android.graphics.Shader;
class CanvasDelegate {
Paint mPaint;
PorterDuffXfermode mMode;
DrawManager mDrawManager;
// DrawManager would do a default draw of the background.
static interface DrawManager {
public void defaultDraw(Canvas canvas);
}
CanvasDelegate(DrawManager drawManager, Mode mode, Paint paint) {
mDrawManager = drawManager;
// DST_IN masks, DST_OUT clips.
mMode = new PorterDuffXfermode(mode);
mPaint = paint;
}
void draw(Canvas canvas, Path path, int width, int height) {
// Save the canvas. All PorterDuff operations should be done in a offscreen bitmap.
int count = canvas.saveLayer(0, 0, width, height, null,
Canvas.MATRIX_SAVE_FLAG |
Canvas.CLIP_SAVE_FLAG |
Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
Canvas.FULL_COLOR_LAYER_SAVE_FLAG |
Canvas.CLIP_TO_LAYER_SAVE_FLAG);
// Do a default draw.
mDrawManager.defaultDraw(canvas);
if (path != null && !path.isEmpty()) {
// ICS added double-buffering, which made it easier for drawing the Path directly over the DST.
// In pre-ICS, drawPath() doesn't seem to use ARGB_8888 mode for performance, hence transparency is not preserved.
mPaint.setXfermode(mMode);
canvas.drawPath(path, mPaint);
}
// Restore the canvas.
canvas.restoreToCount(count);
}
void setShader(Shader shader) {
mPaint.setShader(shader);
}
}

View file

@ -0,0 +1,23 @@
/* 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 android.content.Context;
import android.util.AttributeSet;
public class ForwardButton extends NavButton {
public ForwardButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
mBorderPath.reset();
mBorderPath.moveTo(width - mBorderWidth, 0);
mBorderPath.lineTo(width - mBorderWidth, height);
}
}

View file

@ -0,0 +1,85 @@
/* 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 android.support.v4.content.ContextCompat;
import org.mozilla.gecko.R;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.util.AttributeSet;
abstract class NavButton extends ShapedButton {
protected final Path mBorderPath;
protected final Paint mBorderPaint;
protected final float mBorderWidth;
protected final int mBorderColor;
protected final int mBorderColorPrivate;
public NavButton(Context context, AttributeSet attrs) {
super(context, attrs);
final Resources res = getResources();
mBorderColor = ContextCompat.getColor(context, R.color.disabled_grey);
mBorderColorPrivate = ContextCompat.getColor(context, R.color.toolbar_icon_grey);
mBorderWidth = res.getDimension(R.dimen.nav_button_border_width);
// Paint to draw the border.
mBorderPaint = new Paint();
mBorderPaint.setAntiAlias(true);
mBorderPaint.setStrokeWidth(mBorderWidth);
mBorderPaint.setStyle(Paint.Style.STROKE);
// Path is masked.
mBorderPath = new Path();
setPrivateMode(false);
}
@Override
public void setPrivateMode(boolean isPrivate) {
super.setPrivateMode(isPrivate);
mBorderPaint.setColor(isPrivate ? mBorderColorPrivate : mBorderColor);
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
// Draw the border on top.
canvas.drawPath(mBorderPath, mBorderPaint);
}
// The drawable is constructed as per @drawable/url_bar_nav_button.
@Override
public void onLightweightThemeChanged() {
final Drawable drawable = BrowserToolbar.getLightweightThemeDrawable(this, getTheme(), R.color.toolbar_grey);
if (drawable == null) {
return;
}
final StateListDrawable stateList = new StateListDrawable();
stateList.addState(PRIVATE_PRESSED_STATE_SET, getColorDrawable(R.color.placeholder_active_grey));
stateList.addState(PRESSED_ENABLED_STATE_SET, getColorDrawable(R.color.toolbar_grey_pressed));
stateList.addState(PRIVATE_FOCUSED_STATE_SET, getColorDrawable(R.color.text_and_tabs_tray_grey));
stateList.addState(FOCUSED_STATE_SET, getColorDrawable(R.color.tablet_highlight_focused));
stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.tabs_tray_grey_pressed));
stateList.addState(EMPTY_STATE_SET, drawable);
setBackgroundDrawable(stateList);
}
@Override
public void onLightweightThemeReset() {
setBackgroundResource(R.drawable.url_bar_nav_button);
}
}

View file

@ -0,0 +1,371 @@
/* -*- 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 org.mozilla.gecko.GeckoApp;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.ResourceDrawableUtils;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSObject;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.widget.GeckoPopupMenu;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.ArrayList;
public class PageActionLayout extends LinearLayout implements NativeEventListener,
View.OnClickListener,
View.OnLongClickListener {
private static final String MENU_BUTTON_KEY = "MENU_BUTTON_KEY";
private static final int DEFAULT_PAGE_ACTIONS_SHOWN = 2;
private final Context mContext;
private final LinearLayout mLayout;
private final List<PageAction> mPageActionList;
private GeckoPopupMenu mPageActionsMenu;
// By default it's two, can be changed by calling setNumberShown(int)
private int mMaxVisiblePageActions;
public PageActionLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
mLayout = this;
mPageActionList = new ArrayList<PageAction>();
setNumberShown(DEFAULT_PAGE_ACTIONS_SHOWN);
refreshPageActionIcons();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
"PageActions:Add",
"PageActions:Remove");
}
@Override
protected void onDetachedFromWindow() {
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
"PageActions:Add",
"PageActions:Remove");
super.onDetachedFromWindow();
}
private void setNumberShown(int count) {
ThreadUtils.assertOnUiThread();
mMaxVisiblePageActions = count;
for (int index = 0; index < count; index++) {
if ((getChildCount() - 1) < index) {
mLayout.addView(createImageButton());
}
}
}
@Override
public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
// NativeJSObject cannot be used off of the Gecko thread, so convert it to a Bundle.
final Bundle bundle = message.toBundle();
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
handleUiMessage(event, bundle);
}
});
}
private void handleUiMessage(final String event, final Bundle message) {
ThreadUtils.assertOnUiThread();
if (event.equals("PageActions:Add")) {
final String id = message.getString("id");
final String title = message.getString("title");
final String imageURL = message.getString("icon");
final boolean important = message.getBoolean("important");
addPageAction(id, title, imageURL, new OnPageActionClickListeners() {
@Override
public void onClick(String id) {
GeckoAppShell.notifyObservers("PageActions:Clicked", id);
}
@Override
public boolean onLongClick(String id) {
GeckoAppShell.notifyObservers("PageActions:LongClicked", id);
return true;
}
}, important);
} else if (event.equals("PageActions:Remove")) {
final String id = message.getString("id");
removePageAction(id);
}
}
private void addPageAction(final String id, final String title, final String imageData,
final OnPageActionClickListeners onPageActionClickListeners, boolean important) {
ThreadUtils.assertOnUiThread();
final PageAction pageAction = new PageAction(id, title, null, onPageActionClickListeners, important);
int insertAt = mPageActionList.size();
while (insertAt > 0 && mPageActionList.get(insertAt - 1).isImportant()) {
insertAt--;
}
mPageActionList.add(insertAt, pageAction);
ResourceDrawableUtils.getDrawable(mContext, imageData, new ResourceDrawableUtils.BitmapLoader() {
@Override
public void onBitmapFound(final Drawable d) {
if (mPageActionList.contains(pageAction)) {
pageAction.setDrawable(d);
refreshPageActionIcons();
}
}
});
}
private void removePageAction(String id) {
ThreadUtils.assertOnUiThread();
final Iterator<PageAction> iter = mPageActionList.iterator();
while (iter.hasNext()) {
final PageAction pageAction = iter.next();
if (pageAction.getID().equals(id)) {
iter.remove();
refreshPageActionIcons();
return;
}
}
}
private ImageButton createImageButton() {
ThreadUtils.assertOnUiThread();
final int width = mContext.getResources().getDimensionPixelSize(R.dimen.page_action_button_width);
ImageButton imageButton = new ImageButton(mContext, null, R.style.UrlBar_ImageButton);
imageButton.setLayoutParams(new LayoutParams(width, LayoutParams.MATCH_PARENT));
imageButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
imageButton.setOnClickListener(this);
imageButton.setOnLongClickListener(this);
return imageButton;
}
@Override
public void onClick(View v) {
String buttonClickedId = (String)v.getTag();
if (buttonClickedId != null) {
if (buttonClickedId.equals(MENU_BUTTON_KEY)) {
showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1);
} else {
getPageActionWithId(buttonClickedId).onClick();
}
}
}
@Override
public boolean onLongClick(View v) {
String buttonClickedId = (String)v.getTag();
if (buttonClickedId.equals(MENU_BUTTON_KEY)) {
showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1);
return true;
} else {
return getPageActionWithId(buttonClickedId).onLongClick();
}
}
private void setActionForView(final ImageButton view, final PageAction pageAction) {
ThreadUtils.assertOnUiThread();
if (pageAction == null) {
view.setTag(null);
view.setImageDrawable(null);
view.setVisibility(View.GONE);
view.setContentDescription(null);
return;
}
view.setTag(pageAction.getID());
view.setImageDrawable(pageAction.getDrawable());
view.setVisibility(View.VISIBLE);
view.setContentDescription(pageAction.getTitle());
}
private void refreshPageActionIcons() {
ThreadUtils.assertOnUiThread();
final Resources resources = mContext.getResources();
for (int i = 0; i < this.getChildCount(); i++) {
final ImageButton v = (ImageButton) this.getChildAt(i);
final PageAction pageAction = getPageActionForViewAt(i);
// If there are more page actions than buttons, set the menu icon.
// Otherwise, set the page action's icon if there is a page action.
if ((i == this.getChildCount() - 1) && (mPageActionList.size() > mMaxVisiblePageActions)) {
v.setTag(MENU_BUTTON_KEY);
v.setImageDrawable(resources.getDrawable(R.drawable.icon_pageaction));
v.setVisibility((pageAction != null) ? View.VISIBLE : View.GONE);
v.setContentDescription(resources.getString(R.string.page_action_dropmarker_description));
} else {
setActionForView(v, pageAction);
}
}
}
private PageAction getPageActionForViewAt(int index) {
ThreadUtils.assertOnUiThread();
/**
* We show the user the most recent pageaction added since this keeps the user aware of any new page actions being added
* Also, the order of the pageAction is important i.e. if a page action is added, instead of shifting the pagactions to the
* left to make space for the new one, it would be more visually appealing to have the pageaction appear in the blank space.
*
* buttonIndex is needed for this reason because every new View added to PageActionLayout gets added to the right of its neighbouring View.
* Hence the button on the very leftmost has the index 0. We want our pageactions to start from the rightmost
* and hence we maintain the insertion order of the child Views which is essentially the reverse of their index
*/
final int buttonIndex = (this.getChildCount() - 1) - index;
if (mPageActionList.size() > buttonIndex) {
// Return the pageactions starting from the end of the list for the number of visible pageactions.
final int buttonCount = Math.min(mPageActionList.size(), getChildCount());
return mPageActionList.get((mPageActionList.size() - buttonCount) + buttonIndex);
}
return null;
}
private PageAction getPageActionWithId(String id) {
ThreadUtils.assertOnUiThread();
for (PageAction pageAction : mPageActionList) {
if (pageAction.getID().equals(id)) {
return pageAction;
}
}
return null;
}
private void showMenu(View pageActionButton, int toShow) {
ThreadUtils.assertOnUiThread();
if (mPageActionsMenu == null) {
mPageActionsMenu = new GeckoPopupMenu(pageActionButton.getContext(), pageActionButton);
mPageActionsMenu.inflate(0);
mPageActionsMenu.setOnMenuItemClickListener(new GeckoPopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
int id = item.getItemId();
for (int i = 0; i < mPageActionList.size(); i++) {
PageAction pageAction = mPageActionList.get(i);
if (pageAction.key() == id) {
pageAction.onClick();
return true;
}
}
return false;
}
});
}
Menu menu = mPageActionsMenu.getMenu();
menu.clear();
for (int i = 0; i < mPageActionList.size() && i < toShow; i++) {
PageAction pageAction = mPageActionList.get(i);
MenuItem item = menu.add(Menu.NONE, pageAction.key(), Menu.NONE, pageAction.getTitle());
item.setIcon(pageAction.getDrawable());
}
mPageActionsMenu.show();
}
private static interface OnPageActionClickListeners {
public void onClick(String id);
public boolean onLongClick(String id);
}
private static class PageAction {
private final OnPageActionClickListeners mOnPageActionClickListeners;
private Drawable mDrawable;
private final String mTitle;
private final String mId;
private final int key;
private final boolean mImportant;
public PageAction(String id,
String title,
Drawable image,
OnPageActionClickListeners onPageActionClickListeners,
boolean important) {
mId = id;
mTitle = title;
mDrawable = image;
mOnPageActionClickListeners = onPageActionClickListeners;
mImportant = important;
key = UUID.fromString(mId.subSequence(1, mId.length() - 2).toString()).hashCode();
}
public Drawable getDrawable() {
return mDrawable;
}
public void setDrawable(Drawable d) {
mDrawable = d;
}
public String getTitle() {
return mTitle;
}
public String getID() {
return mId;
}
public int key() {
return key;
}
public boolean isImportant() {
return mImportant;
}
public void onClick() {
if (mOnPageActionClickListeners != null) {
mOnPageActionClickListeners.onClick(mId);
}
}
public boolean onLongClick() {
if (mOnPageActionClickListeners != null) {
return mOnPageActionClickListeners.onLongClick(mId);
}
return false;
}
}
}

View file

@ -0,0 +1,29 @@
/* 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 android.content.Context;
import android.util.AttributeSet;
import org.mozilla.gecko.tabs.TabCurve;
public class PhoneTabsButton extends ShapedButton {
public PhoneTabsButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
mPath.reset();
mPath.moveTo(0, 0);
TabCurve.drawFromTop(mPath, 0, height, TabCurve.Direction.RIGHT);
mPath.lineTo(width, height);
mPath.lineTo(width, 0);
mPath.lineTo(0, 0);
}
}

View file

@ -0,0 +1,109 @@
/* 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 android.support.v4.content.ContextCompat;
import org.mozilla.gecko.R;
import org.mozilla.gecko.lwt.LightweightThemeDrawable;
import org.mozilla.gecko.widget.themed.ThemedImageButton;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff.Mode;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.util.AttributeSet;
/**
* A ImageButton with a custom drawn path and lightweight theme support. Note that {@link ShapedButtonFrameLayout}
* copies the lwt support so if you change it here, you should probably change it there.
*/
public class ShapedButton extends ThemedImageButton
implements CanvasDelegate.DrawManager {
protected final Path mPath;
protected final CanvasDelegate mCanvasDelegate;
public ShapedButton(Context context, AttributeSet attrs) {
super(context, attrs);
// Path is clipped.
mPath = new Path();
final Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(ContextCompat.getColor(context, R.color.canvas_delegate_paint));
paint.setStrokeWidth(0.0f);
mCanvasDelegate = new CanvasDelegate(this, Mode.DST_IN, paint);
setWillNotDraw(false);
}
@Override
@SuppressLint("MissingSuperCall") // Super gets called from defaultDraw().
// It is intentionally not called in the other case.
public void draw(Canvas canvas) {
if (mCanvasDelegate != null)
mCanvasDelegate.draw(canvas, mPath, getWidth(), getHeight());
else
defaultDraw(canvas);
}
@Override
public void defaultDraw(Canvas canvas) {
super.draw(canvas);
}
// The drawable is constructed as per @drawable/shaped_button.
@Override
public void onLightweightThemeChanged() {
final int background = ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey);
final LightweightThemeDrawable lightWeight = getTheme().getColorDrawable(this, background);
if (lightWeight == null)
return;
lightWeight.setAlpha(34, 34);
final StateListDrawable stateList = new StateListDrawable();
stateList.addState(PRESSED_ENABLED_STATE_SET, getColorDrawable(R.color.highlight_shaped));
stateList.addState(FOCUSED_STATE_SET, getColorDrawable(R.color.highlight_shaped_focused));
stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.text_and_tabs_tray_grey));
stateList.addState(EMPTY_STATE_SET, lightWeight);
setBackgroundDrawable(stateList);
}
@Override
public void onLightweightThemeReset() {
setBackgroundResource(R.drawable.shaped_button);
}
@Override
public void setBackgroundDrawable(Drawable drawable) {
if (getBackground() == null || drawable == null) {
super.setBackgroundDrawable(drawable);
return;
}
int[] padding = new int[] { getPaddingLeft(),
getPaddingTop(),
getPaddingRight(),
getPaddingBottom()
};
drawable.setLevel(getBackground().getLevel());
super.setBackgroundDrawable(drawable);
setPadding(padding[0], padding[1], padding[2], padding[3]);
}
@Override
public void setBackgroundResource(int resId) {
setBackgroundDrawable(getResources().getDrawable(resId));
}
}

View file

@ -0,0 +1,74 @@
/* 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 android.support.v4.content.ContextCompat;
import org.mozilla.gecko.R;
import org.mozilla.gecko.lwt.LightweightThemeDrawable;
import org.mozilla.gecko.widget.themed.ThemedFrameLayout;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.util.AttributeSet;
/** A FrameLayout with lightweight theme support. Note that {@link ShapedButton}'s lwt support is basically the same so
* if you change it here, you should probably change it there. Note also that this doesn't have ShapedButton's path code
* so shouldn't have "ShapedButton" in the name, but I wanted to make the connection apparent so I left it.
*/
public class ShapedButtonFrameLayout extends ThemedFrameLayout {
public ShapedButtonFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
// The drawable is constructed as per @drawable/shaped_button.
@Override
public void onLightweightThemeChanged() {
final int background = ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey);
final LightweightThemeDrawable lightWeight = getTheme().getColorDrawable(this, background);
if (lightWeight == null)
return;
lightWeight.setAlpha(34, 34);
final StateListDrawable stateList = new StateListDrawable();
stateList.addState(PRESSED_ENABLED_STATE_SET, getColorDrawable(R.color.highlight_shaped));
stateList.addState(FOCUSED_STATE_SET, getColorDrawable(R.color.highlight_shaped_focused));
stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.text_and_tabs_tray_grey));
stateList.addState(EMPTY_STATE_SET, lightWeight);
setBackgroundDrawable(stateList);
}
@Override
public void onLightweightThemeReset() {
setBackgroundResource(R.drawable.shaped_button);
}
@Override
public void setBackgroundDrawable(Drawable drawable) {
if (getBackground() == null || drawable == null) {
super.setBackgroundDrawable(drawable);
return;
}
int[] padding = new int[] { getPaddingLeft(),
getPaddingTop(),
getPaddingRight(),
getPaddingBottom()
};
drawable.setLevel(getBackground().getLevel());
super.setBackgroundDrawable(drawable);
setPadding(padding[0], padding[1], padding[2], padding[3]);
}
@Override
public void setBackgroundResource(int resId) {
setBackgroundDrawable(getResources().getDrawable(resId));
}
}

View file

@ -0,0 +1,571 @@
/* 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 android.content.ClipData;
import android.content.ClipboardManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
import android.widget.ImageView;
import android.widget.Toast;
import org.json.JSONException;
import org.json.JSONArray;
import org.mozilla.gecko.AboutPages;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.R;
import org.mozilla.gecko.GeckoApp;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.SiteIdentity;
import org.mozilla.gecko.SiteIdentity.SecurityMode;
import org.mozilla.gecko.SiteIdentity.MixedMode;
import org.mozilla.gecko.SiteIdentity.TrackingMode;
import org.mozilla.gecko.SnackbarBuilder;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.widget.AnchoredPopup;
import org.mozilla.gecko.widget.DoorHanger;
import org.mozilla.gecko.widget.DoorHanger.OnButtonClickListener;
import org.json.JSONObject;
import android.app.Activity;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.mozilla.gecko.widget.DoorhangerConfig;
import org.mozilla.gecko.widget.SiteLogins;
/**
* SiteIdentityPopup is a singleton class that displays site identity data in
* an arrow panel popup hanging from the lock icon in the browser toolbar.
*
* A site identity icon may be displayed in the url, and is set in <code>ToolbarDisplayLayout</code>.
*/
public class SiteIdentityPopup extends AnchoredPopup implements GeckoEventListener {
public static enum ButtonType { DISABLE, ENABLE, KEEP_BLOCKING, CANCEL, COPY }
private static final String LOGTAG = "GeckoSiteIdentityPopup";
private static final String MIXED_CONTENT_SUPPORT_URL =
"https://support.mozilla.org/kb/how-does-insecure-content-affect-safety-android";
private static final String TRACKING_CONTENT_SUPPORT_URL =
"https://support.mozilla.org/kb/firefox-android-tracking-protection";
// Placeholder string.
private final static String FORMAT_S = "%s";
private final Resources mResources;
private SiteIdentity mSiteIdentity;
private LinearLayout mIdentity;
private LinearLayout mIdentityKnownContainer;
private ImageView mIcon;
private TextView mTitle;
private TextView mSecurityState;
private TextView mMixedContentActivity;
private TextView mOwner;
private TextView mOwnerSupplemental;
private TextView mVerifier;
private TextView mLink;
private TextView mSiteSettingsLink;
private View mDivider;
private DoorHanger mTrackingContentNotification;
private DoorHanger mSelectLoginDoorhanger;
private final OnButtonClickListener mContentButtonClickListener;
public SiteIdentityPopup(Context context) {
super(context);
mResources = mContext.getResources();
mContentButtonClickListener = new ContentNotificationButtonListener();
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
"Doorhanger:Logins",
"Permissions:CheckResult");
}
@Override
protected void init() {
super.init();
// Make the popup focusable so it doesn't inadvertently trigger click events elsewhere
// which may reshow the popup (see bug 785156)
setFocusable(true);
LayoutInflater inflater = LayoutInflater.from(mContext);
mIdentity = (LinearLayout) inflater.inflate(R.layout.site_identity, null);
mContent.addView(mIdentity);
mIdentityKnownContainer =
(LinearLayout) mIdentity.findViewById(R.id.site_identity_known_container);
mIcon = (ImageView) mIdentity.findViewById(R.id.site_identity_icon);
mTitle = (TextView) mIdentity.findViewById(R.id.site_identity_title);
mSecurityState = (TextView) mIdentity.findViewById(R.id.site_identity_state);
mMixedContentActivity = (TextView) mIdentity.findViewById(R.id.mixed_content_activity);
mOwner = (TextView) mIdentityKnownContainer.findViewById(R.id.owner);
mOwnerSupplemental = (TextView) mIdentityKnownContainer.findViewById(R.id.owner_supplemental);
mVerifier = (TextView) mIdentityKnownContainer.findViewById(R.id.verifier);
mDivider = mIdentity.findViewById(R.id.divider_doorhanger);
mLink = (TextView) mIdentity.findViewById(R.id.site_identity_link);
mLink.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Tabs.getInstance().loadUrlInTab(MIXED_CONTENT_SUPPORT_URL);
}
});
mSiteSettingsLink = (TextView) mIdentity.findViewById(R.id.site_settings_link);
}
private void updateIdentity(final SiteIdentity siteIdentity) {
if (!mInflated) {
init();
}
final boolean isIdentityKnown = (siteIdentity.getSecurityMode() == SecurityMode.IDENTIFIED ||
siteIdentity.getSecurityMode() == SecurityMode.VERIFIED);
updateConnectionState(siteIdentity);
toggleIdentityKnownContainerVisibility(isIdentityKnown);
if (isIdentityKnown) {
updateIdentityInformation(siteIdentity);
}
GeckoAppShell.notifyObservers("Permissions:Check", null);
}
@Override
public void handleMessage(String event, JSONObject geckoObject) {
if ("Doorhanger:Logins".equals(event)) {
try {
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
if (selectedTab != null) {
final JSONObject data = geckoObject.getJSONObject("data");
addLoginsToTab(data);
}
if (isShowing()) {
addSelectLoginDoorhanger(selectedTab);
}
} catch (JSONException e) {
Log.e(LOGTAG, "Error accessing logins in Doorhanger:Logins message", e);
}
} else if ("Permissions:CheckResult".equals(event)) {
final boolean hasPermissions = geckoObject.optBoolean("hasPermissions", false);
if (hasPermissions) {
mSiteSettingsLink.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GeckoAppShell.notifyObservers("Permissions:Get", null);
dismiss();
}
});
}
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
mSiteSettingsLink.setVisibility(hasPermissions ? View.VISIBLE : View.GONE);
}
});
}
}
private void addLoginsToTab(JSONObject data) throws JSONException {
final JSONArray logins = data.getJSONArray("logins");
final SiteLogins siteLogins = new SiteLogins(logins);
Tabs.getInstance().getSelectedTab().setSiteLogins(siteLogins);
}
private void addSelectLoginDoorhanger(Tab tab) throws JSONException {
final SiteLogins siteLogins = tab.getSiteLogins();
if (siteLogins == null) {
return;
}
final JSONArray logins = siteLogins.getLogins();
if (logins.length() == 0) {
return;
}
final JSONObject login = (JSONObject) logins.get(0);
// Create button click listener for copying a password to the clipboard.
final OnButtonClickListener buttonClickListener = new OnButtonClickListener() {
Activity activity = (Activity) mContext;
@Override
public void onButtonClick(JSONObject response, DoorHanger doorhanger) {
try {
final int buttonId = response.getInt("callback");
if (buttonId == ButtonType.COPY.ordinal()) {
final ClipboardManager manager = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
String password;
if (response.has("password")) {
// Click listener being called from List Dialog.
password = response.optString("password");
} else {
password = login.getString("password");
}
manager.setPrimaryClip(ClipData.newPlainText("password", password));
SnackbarBuilder.builder(activity)
.message(R.string.doorhanger_login_select_toast_copy)
.duration(Snackbar.LENGTH_SHORT)
.buildAndShow();
}
dismiss();
} catch (JSONException e) {
Log.e(LOGTAG, "Error handling Select login button click", e);
SnackbarBuilder.builder(activity)
.message(R.string.doorhanger_login_select_toast_copy_error)
.duration(Snackbar.LENGTH_SHORT)
.buildAndShow();
}
}
};
final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.LOGIN, buttonClickListener);
// Set buttons.
config.setButton(mContext.getString(R.string.button_cancel), ButtonType.CANCEL.ordinal(), false);
config.setButton(mContext.getString(R.string.button_copy), ButtonType.COPY.ordinal(), true);
// Set message.
String username = ((JSONObject) logins.get(0)).getString("username");
if (TextUtils.isEmpty(username)) {
username = mContext.getString(R.string.doorhanger_login_no_username);
}
final String message = mContext.getString(R.string.doorhanger_login_select_message).replace(FORMAT_S, username);
config.setMessage(message);
// Set options.
final JSONObject options = new JSONObject();
// Add action text only if there are other logins to select.
if (logins.length() > 1) {
final JSONObject actionText = new JSONObject();
actionText.put("type", "SELECT");
final JSONObject bundle = new JSONObject();
bundle.put("logins", logins);
actionText.put("bundle", bundle);
options.put("actionText", actionText);
}
config.setOptions(options);
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
if (!mInflated) {
init();
}
removeSelectLoginDoorhanger();
mSelectLoginDoorhanger = DoorHanger.Get(mContext, config);
mContent.addView(mSelectLoginDoorhanger);
mDivider.setVisibility(View.VISIBLE);
}
});
}
private void removeSelectLoginDoorhanger() {
if (mSelectLoginDoorhanger != null) {
mContent.removeView(mSelectLoginDoorhanger);
mSelectLoginDoorhanger = null;
}
}
private void toggleIdentityKnownContainerVisibility(final boolean isIdentityKnown) {
final int identityInfoVisibility = isIdentityKnown ? View.VISIBLE : View.GONE;
mIdentityKnownContainer.setVisibility(identityInfoVisibility);
}
/**
* Update the Site Identity content to reflect connection state.
*
* The connection state should reflect the combination of:
* a) Connection encryption
* b) Mixed Content state (Active/Display Mixed content, loaded, blocked, none, etc)
* and update the icons and strings to inform the user of that state.
*
* @param siteIdentity SiteIdentity information about the connection.
*/
private void updateConnectionState(final SiteIdentity siteIdentity) {
if (siteIdentity.getSecurityMode() == SecurityMode.CHROMEUI) {
mSecurityState.setText(R.string.identity_connection_chromeui);
mSecurityState.setTextColor(ContextCompat.getColor(mContext, R.color.placeholder_active_grey));
mIcon.setImageResource(R.drawable.icon);
clearSecurityStateIcon();
mMixedContentActivity.setVisibility(View.GONE);
mLink.setVisibility(View.GONE);
} else if (!siteIdentity.isSecure()) {
if (siteIdentity.getMixedModeActive() == MixedMode.MIXED_CONTENT_LOADED) {
// Active Mixed Content loaded because user has disabled blocking.
mIcon.setImageResource(R.drawable.lock_disabled);
clearSecurityStateIcon();
mMixedContentActivity.setVisibility(View.VISIBLE);
mMixedContentActivity.setText(R.string.mixed_content_protection_disabled);
mLink.setVisibility(View.VISIBLE);
} else if (siteIdentity.getMixedModeDisplay() == MixedMode.MIXED_CONTENT_LOADED) {
// Passive Mixed Content loaded.
mIcon.setImageResource(R.drawable.lock_inactive);
setSecurityStateIcon(R.drawable.warning_major, 1);
mMixedContentActivity.setVisibility(View.VISIBLE);
if (siteIdentity.getMixedModeActive() == MixedMode.MIXED_CONTENT_BLOCKED) {
mMixedContentActivity.setText(R.string.mixed_content_blocked_some);
} else {
mMixedContentActivity.setText(R.string.mixed_content_display_loaded);
}
mLink.setVisibility(View.VISIBLE);
} else {
// Unencrypted connection with no mixed content.
mIcon.setImageResource(R.drawable.globe_light);
clearSecurityStateIcon();
mMixedContentActivity.setVisibility(View.GONE);
mLink.setVisibility(View.GONE);
}
mSecurityState.setText(R.string.identity_connection_insecure);
mSecurityState.setTextColor(ContextCompat.getColor(mContext, R.color.placeholder_active_grey));
} else {
// Connection is secure.
mIcon.setImageResource(R.drawable.lock_secure);
setSecurityStateIcon(R.drawable.img_check, 2);
mSecurityState.setTextColor(ContextCompat.getColor(mContext, R.color.affirmative_green));
mSecurityState.setText(R.string.identity_connection_secure);
// Mixed content has been blocked, if present.
if (siteIdentity.getMixedModeActive() == MixedMode.MIXED_CONTENT_BLOCKED ||
siteIdentity.getMixedModeDisplay() == MixedMode.MIXED_CONTENT_BLOCKED) {
mMixedContentActivity.setVisibility(View.VISIBLE);
mMixedContentActivity.setText(R.string.mixed_content_blocked_all);
mLink.setVisibility(View.VISIBLE);
} else {
mMixedContentActivity.setVisibility(View.GONE);
mLink.setVisibility(View.GONE);
}
}
}
private void clearSecurityStateIcon() {
mSecurityState.setCompoundDrawablePadding(0);
mSecurityState.setCompoundDrawables(null, null, null, null);
}
private void setSecurityStateIcon(int resource, int factor) {
final Drawable stateIcon = ContextCompat.getDrawable(mContext, resource);
stateIcon.setBounds(0, 0, stateIcon.getIntrinsicWidth() / factor, stateIcon.getIntrinsicHeight() / factor);
mSecurityState.setCompoundDrawables(stateIcon, null, null, null);
mSecurityState.setCompoundDrawablePadding((int) mResources.getDimension(R.dimen.doorhanger_drawable_padding));
}
private void updateIdentityInformation(final SiteIdentity siteIdentity) {
String owner = siteIdentity.getOwner();
if (owner == null) {
mOwner.setVisibility(View.GONE);
mOwnerSupplemental.setVisibility(View.GONE);
} else {
mOwner.setVisibility(View.VISIBLE);
mOwner.setText(owner);
// Supplemental data is optional.
final String supplemental = siteIdentity.getSupplemental();
if (!TextUtils.isEmpty(supplemental)) {
mOwnerSupplemental.setText(supplemental);
mOwnerSupplemental.setVisibility(View.VISIBLE);
} else {
mOwnerSupplemental.setVisibility(View.GONE);
}
}
final String verifier = siteIdentity.getVerifier();
mVerifier.setText(verifier);
}
private void addTrackingContentNotification(boolean blocked) {
// Remove any existing tracking content notification.
removeTrackingContentNotification();
final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.TRACKING, mContentButtonClickListener);
final int icon = blocked ? R.drawable.shield_enabled : R.drawable.shield_disabled;
final JSONObject options = new JSONObject();
final JSONObject tracking = new JSONObject();
try {
tracking.put("enabled", blocked);
options.put("tracking_protection", tracking);
} catch (JSONException e) {
Log.e(LOGTAG, "Error adding tracking protection options", e);
}
config.setOptions(options);
config.setLink(mContext.getString(R.string.learn_more), TRACKING_CONTENT_SUPPORT_URL);
addNotificationButtons(config, blocked);
mTrackingContentNotification = DoorHanger.Get(mContext, config);
mTrackingContentNotification.setIcon(icon);
mContent.addView(mTrackingContentNotification);
mDivider.setVisibility(View.VISIBLE);
}
private void removeTrackingContentNotification() {
if (mTrackingContentNotification != null) {
mContent.removeView(mTrackingContentNotification);
mTrackingContentNotification = null;
}
}
private void addNotificationButtons(DoorhangerConfig config, boolean blocked) {
if (blocked) {
config.setButton(mContext.getString(R.string.disable_protection), ButtonType.DISABLE.ordinal(), false);
} else {
config.setButton(mContext.getString(R.string.enable_protection), ButtonType.ENABLE.ordinal(), true);
}
}
/*
* @param identityData A JSONObject that holds the current tab's identity data.
*/
void setSiteIdentity(SiteIdentity siteIdentity) {
mSiteIdentity = siteIdentity;
}
@Override
public void show() {
if (mSiteIdentity == null) {
Log.e(LOGTAG, "Can't show site identity popup for undefined state");
return;
}
// Verified about: pages have the CHROMEUI SiteIdentity, however there can also
// be unverified about: pages for which "This site's identity is unknown" or
// "This is a secure Firefox page" are both misleading, so don't show a popup.
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
if (selectedTab != null &&
AboutPages.isAboutPage(selectedTab.getURL()) &&
mSiteIdentity.getSecurityMode() != SecurityMode.CHROMEUI) {
Log.d(LOGTAG, "We don't show site identity popups for unverified about: pages");
return;
}
updateIdentity(mSiteIdentity);
final TrackingMode trackingMode = mSiteIdentity.getTrackingMode();
if (trackingMode != TrackingMode.UNKNOWN) {
addTrackingContentNotification(trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED);
}
try {
addSelectLoginDoorhanger(selectedTab);
} catch (JSONException e) {
Log.e(LOGTAG, "Error adding selectLogin doorhanger", e);
}
if (mSiteIdentity.getSecurityMode() == SecurityMode.CHROMEUI) {
// For about: pages we display the product icon in place of the verified/globe
// image, hence we don't also set the favicon (for most about pages the
// favicon is the product icon, hence we'd be showing the same icon twice).
mTitle.setText(R.string.moz_app_displayname);
} else {
mTitle.setText(selectedTab.getBaseDomain());
final Bitmap favicon = selectedTab.getFavicon();
if (favicon != null) {
final Drawable faviconDrawable = new BitmapDrawable(mResources, favicon);
final int dimen = (int) mResources.getDimension(R.dimen.browser_toolbar_favicon_size);
faviconDrawable.setBounds(0, 0, dimen, dimen);
mTitle.setCompoundDrawables(faviconDrawable, null, null, null);
mTitle.setCompoundDrawablePadding((int) mContext.getResources().getDimension(R.dimen.doorhanger_drawable_padding));
}
}
showDividers();
super.show();
}
// Show the right dividers
private void showDividers() {
final int count = mContent.getChildCount();
DoorHanger lastVisibleDoorHanger = null;
for (int i = 0; i < count; i++) {
final View child = mContent.getChildAt(i);
if (!(child instanceof DoorHanger)) {
continue;
}
DoorHanger dh = (DoorHanger) child;
dh.showDivider();
if (dh.getVisibility() == View.VISIBLE) {
lastVisibleDoorHanger = dh;
}
}
if (lastVisibleDoorHanger != null) {
lastVisibleDoorHanger.hideDivider();
}
}
void destroy() {
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
"Doorhanger:Logins",
"Permissions:CheckResult");
}
@Override
public void dismiss() {
super.dismiss();
removeTrackingContentNotification();
removeSelectLoginDoorhanger();
mTitle.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
mDivider.setVisibility(View.GONE);
}
private class ContentNotificationButtonListener implements OnButtonClickListener {
@Override
public void onButtonClick(JSONObject response, DoorHanger doorhanger) {
GeckoAppShell.notifyObservers("Session:Reload", response.toString());
dismiss();
}
}
}

View file

@ -0,0 +1,154 @@
/* -*- 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 org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.R;
import org.mozilla.gecko.animation.Rotate3DAnimation;
import org.mozilla.gecko.widget.themed.ThemedTextSwitcher;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.AlphaAnimation;
import android.view.animation.AnimationSet;
import android.widget.ViewSwitcher;
public class TabCounter extends ThemedTextSwitcher
implements ViewSwitcher.ViewFactory {
private static final float CENTER_X = 0.5f;
private static final float CENTER_Y = 1.25f;
private static final int DURATION = 500;
private static final float Z_DISTANCE = 200;
private final AnimationSet mFlipInForward;
private final AnimationSet mFlipInBackward;
private final AnimationSet mFlipOutForward;
private final AnimationSet mFlipOutBackward;
private final LayoutInflater mInflater;
private final int mLayoutId;
private int mCount;
public static final int MAX_VISIBLE_TABS = 99;
public static final String SO_MANY_TABS_OPEN = "";
private enum FadeMode {
FADE_IN,
FADE_OUT
}
public TabCounter(Context context, AttributeSet attrs) {
super(context, attrs);
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabCounter);
mLayoutId = a.getResourceId(R.styleable.TabCounter_android_layout, R.layout.tabs_counter);
a.recycle();
mInflater = LayoutInflater.from(context);
mFlipInForward = createAnimation(-90, 0, FadeMode.FADE_IN, -1 * Z_DISTANCE, false);
mFlipInBackward = createAnimation(90, 0, FadeMode.FADE_IN, Z_DISTANCE, false);
mFlipOutForward = createAnimation(0, -90, FadeMode.FADE_OUT, -1 * Z_DISTANCE, true);
mFlipOutBackward = createAnimation(0, 90, FadeMode.FADE_OUT, Z_DISTANCE, true);
removeAllViews();
setFactory(this);
if (Versions.feature16Plus) {
// This adds the TextSwitcher to the a11y node tree, where we in turn
// could make it return an empty info node. If we don't do this the
// TextSwitcher's child TextViews get picked up, and we don't want
// that since the tabs ImageButton is already properly labeled for
// accessibility.
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
setAccessibilityDelegate(new View.AccessibilityDelegate() {
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {}
});
}
}
void setCountWithAnimation(int count) {
// Don't animate from initial state
if (mCount == 0) {
setCount(count);
return;
}
if (mCount == count) {
return;
}
// don't animate if there are still over MAX_VISIBLE_TABS tabs open
if (mCount > MAX_VISIBLE_TABS && count > MAX_VISIBLE_TABS) {
mCount = count;
return;
}
if (count < mCount) {
setInAnimation(mFlipInBackward);
setOutAnimation(mFlipOutForward);
} else {
setInAnimation(mFlipInForward);
setOutAnimation(mFlipOutBackward);
}
// Eliminate screen artifact. Set explicit In/Out animation pair order. This will always
// animate pair in In->Out child order, prevent alternating use of the Out->In case.
setDisplayedChild(0);
// Set In value, trigger animation to Out value
setCurrentText(formatForDisplay(mCount));
setText(formatForDisplay(count));
mCount = count;
}
private String formatForDisplay(int count) {
if (count > MAX_VISIBLE_TABS) {
return SO_MANY_TABS_OPEN;
}
return String.valueOf(count);
}
void setCount(int count) {
setCurrentText(formatForDisplay(count));
mCount = count;
}
// Alpha animations in editing mode cause action bar corruption on the
// Nexus 7 (bug 961749). As a workaround, skip these animations in editing
// mode.
void onEnterEditingMode() {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
getChildAt(i).clearAnimation();
}
}
private AnimationSet createAnimation(float startAngle, float endAngle,
FadeMode fadeMode,
float zEnd, boolean reverse) {
final Context context = getContext();
AnimationSet set = new AnimationSet(context, null);
set.addAnimation(new Rotate3DAnimation(startAngle, endAngle, CENTER_X, CENTER_Y, zEnd, reverse));
set.addAnimation(fadeMode == FadeMode.FADE_IN ? new AlphaAnimation(0.0f, 1.0f) :
new AlphaAnimation(1.0f, 0.0f));
set.setDuration(DURATION);
set.setInterpolator(context, android.R.anim.accelerate_interpolator);
return set;
}
@Override
public View makeView() {
return mInflater.inflate(mLayoutId, null);
}
}

View file

@ -0,0 +1,530 @@
/* -*- 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.Arrays;
import java.util.EnumSet;
import java.util.List;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.AboutPages;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.R;
import org.mozilla.gecko.reader.ReaderModeUtils;
import org.mozilla.gecko.SiteIdentity;
import org.mozilla.gecko.SiteIdentity.MixedMode;
import org.mozilla.gecko.SiteIdentity.SecurityMode;
import org.mozilla.gecko.SiteIdentity.TrackingMode;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.toolbar.BrowserToolbarTabletBase.ForwardButtonAnimation;
import org.mozilla.gecko.Experiments;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.StringUtils;
import org.mozilla.gecko.widget.themed.ThemedLinearLayout;
import org.mozilla.gecko.widget.themed.ThemedTextView;
import android.content.Context;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ImageButton;
import com.keepsafe.switchboard.SwitchBoard;
/**
* {@code ToolbarDisplayLayout} is the UI for when the toolbar is in
* display state. It's used to display the state of the currently selected
* tab. It should always be updated through a single entry point
* (updateFromTab) and should never track any tab events or gecko messages
* on its own to keep it as dumb as possible.
*
* The UI has two possible modes: progress and display which are triggered
* when UpdateFlags.PROGRESS is used depending on the current tab state.
* The progress mode is triggered when the tab is loading a page. Display mode
* is used otherwise.
*
* {@code ToolbarDisplayLayout} is meant to be owned by {@code BrowserToolbar}
* which is the main event bus for the toolbar subsystem.
*/
public class ToolbarDisplayLayout extends ThemedLinearLayout {
private static final String LOGTAG = "GeckoToolbarDisplayLayout";
private boolean mTrackingProtectionEnabled;
// To be used with updateFromTab() to allow the caller
// to give enough context for the requested state change.
enum UpdateFlags {
TITLE,
FAVICON,
PROGRESS,
SITE_IDENTITY,
PRIVATE_MODE,
// Disable any animation that might be
// triggered from this state change. Mostly
// used on tab switches, see BrowserToolbar.
DISABLE_ANIMATIONS
}
private enum UIMode {
PROGRESS,
DISPLAY
}
interface OnStopListener {
Tab onStop();
}
interface OnTitleChangeListener {
void onTitleChange(CharSequence title);
}
private final BrowserApp mActivity;
private UIMode mUiMode;
private boolean mIsAttached;
private final ThemedTextView mTitle;
private final int mTitlePadding;
private ToolbarPrefs mPrefs;
private OnTitleChangeListener mTitleChangeListener;
private final ImageButton mSiteSecurity;
private final ImageButton mStop;
private OnStopListener mStopListener;
private final PageActionLayout mPageActionLayout;
private final SiteIdentityPopup mSiteIdentityPopup;
private int mSecurityImageLevel;
// Security level constants, which map to the icons / levels defined in:
// http://dxr.mozilla.org/mozilla-central/source/mobile/android/base/java/org/mozilla/gecko/resources/drawable/site_security_level.xml
// Default level (unverified pages) - globe icon:
private static final int LEVEL_DEFAULT_GLOBE = 0;
// Levels for displaying Mixed Content state icons.
private static final int LEVEL_WARNING_MINOR = 3;
private static final int LEVEL_LOCK_DISABLED = 4;
// Levels for displaying Tracking Protection state icons.
private static final int LEVEL_SHIELD_ENABLED = 5;
private static final int LEVEL_SHIELD_DISABLED = 6;
// Icon used for about:home
private static final int LEVEL_SEARCH_ICON = 999;
private final ForegroundColorSpan mUrlColorSpan;
private final ForegroundColorSpan mPrivateUrlColorSpan;
private final ForegroundColorSpan mBlockedColorSpan;
private final ForegroundColorSpan mDomainColorSpan;
private final ForegroundColorSpan mPrivateDomainColorSpan;
private final ForegroundColorSpan mCertificateOwnerColorSpan;
public ToolbarDisplayLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setOrientation(HORIZONTAL);
mActivity = (BrowserApp) context;
LayoutInflater.from(context).inflate(R.layout.toolbar_display_layout, this);
mTitle = (ThemedTextView) findViewById(R.id.url_bar_title);
mTitlePadding = mTitle.getPaddingRight();
mUrlColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_urltext));
mPrivateUrlColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_urltext_private));
mBlockedColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_blockedtext));
mDomainColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_domaintext));
mPrivateDomainColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_domaintext_private));
mCertificateOwnerColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.affirmative_green));
mSiteSecurity = (ImageButton) findViewById(R.id.site_security);
mSiteIdentityPopup = new SiteIdentityPopup(mActivity);
mSiteIdentityPopup.setAnchor(this);
mSiteIdentityPopup.setOnVisibilityChangeListener(mActivity);
mStop = (ImageButton) findViewById(R.id.stop);
mPageActionLayout = (PageActionLayout) findViewById(R.id.page_action_layout);
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
mIsAttached = true;
mSiteSecurity.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View view) {
mSiteIdentityPopup.show();
}
});
mStop.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View v) {
if (mStopListener != null) {
// Force toolbar to switch to Display mode
// immediately based on the stopped tab.
final Tab tab = mStopListener.onStop();
if (tab != null) {
updateUiMode(UIMode.DISPLAY);
}
}
}
});
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
mIsAttached = false;
}
@Override
public void setNextFocusDownId(int nextId) {
mStop.setNextFocusDownId(nextId);
mSiteSecurity.setNextFocusDownId(nextId);
mPageActionLayout.setNextFocusDownId(nextId);
}
void setToolbarPrefs(final ToolbarPrefs prefs) {
mPrefs = prefs;
}
void updateFromTab(@NonNull Tab tab, EnumSet<UpdateFlags> flags) {
// Several parts of ToolbarDisplayLayout's state depends
// on the views being attached to the view tree.
if (!mIsAttached) {
return;
}
if (flags.contains(UpdateFlags.TITLE)) {
updateTitle(tab);
}
if (flags.contains(UpdateFlags.SITE_IDENTITY)) {
updateSiteIdentity(tab);
}
if (flags.contains(UpdateFlags.PROGRESS)) {
updateProgress(tab);
}
if (flags.contains(UpdateFlags.PRIVATE_MODE)) {
mTitle.setPrivateMode(tab.isPrivate());
}
}
void setTitle(CharSequence title) {
mTitle.setText(title);
if (mTitleChangeListener != null) {
mTitleChangeListener.onTitleChange(title);
}
}
private void updateTitle(@NonNull Tab tab) {
// Keep the title unchanged if there's no selected tab,
// or if the tab is entering reader mode.
if (tab.isEnteringReaderMode()) {
return;
}
final String url = tab.getURL();
// Setting a null title will ensure we just see the
// "Enter Search or Address" placeholder text.
if (AboutPages.isTitlelessAboutPage(url)) {
setTitle(null);
setContentDescription(null);
return;
}
// Show the about:blocked page title in red, regardless of prefs
if (tab.getErrorType() == Tab.ErrorType.BLOCKED) {
final String title = tab.getDisplayTitle();
final SpannableStringBuilder builder = new SpannableStringBuilder(title);
builder.setSpan(mBlockedColorSpan, 0, title.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
setTitle(builder);
setContentDescription(null);
return;
}
final String baseDomain = tab.getBaseDomain();
String strippedURL = stripAboutReaderURL(url);
final boolean isHttpOrHttps = StringUtils.isHttpOrHttps(strippedURL);
if (mPrefs.shouldTrimUrls()) {
strippedURL = StringUtils.stripCommonSubdomains(StringUtils.stripScheme(strippedURL));
}
// The URL bar does not support RTL currently (See bug 928688 and meta bug 702845).
// Displaying a URL using RTL (or mixed) characters can lead to an undesired reordering
// of elements of the URL. That's why we are forcing the URL to use LTR (bug 1284372).
strippedURL = StringUtils.forceLTR(strippedURL);
// This value is not visible to screen readers but we rely on it when running UI tests. Screen
// readers will instead focus BrowserToolbar and read the "base domain" from there. UI tests
// will read the content description to obtain the full URL for performing assertions.
setContentDescription(strippedURL);
final SiteIdentity siteIdentity = tab.getSiteIdentity();
if (siteIdentity.hasOwner() && SwitchBoard.isInExperiment(mActivity, Experiments.URLBAR_SHOW_EV_CERT_OWNER)) {
// Show Owner of EV certificate as title
updateTitleFromSiteIdentity(siteIdentity);
} else if (isHttpOrHttps && !HardwareUtils.isTablet() && !TextUtils.isEmpty(baseDomain)
&& SwitchBoard.isInExperiment(mActivity, Experiments.URLBAR_SHOW_ORIGIN_ONLY)) {
// Show just the base domain as title
setTitle(baseDomain);
} else {
// Display full URL with base domain highlighted as title
updateAndColorTitleFromFullURL(strippedURL, baseDomain, tab.isPrivate());
}
}
private void updateTitleFromSiteIdentity(SiteIdentity siteIdentity) {
final String title;
if (siteIdentity.hasCountry()) {
title = String.format("%s (%s)", siteIdentity.getOwner(), siteIdentity.getCountry());
} else {
title = siteIdentity.getOwner();
}
final SpannableString spannable = new SpannableString(title);
spannable.setSpan(mCertificateOwnerColorSpan, 0, title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
setTitle(spannable);
}
private void updateAndColorTitleFromFullURL(String url, String baseDomain, boolean isPrivate) {
if (TextUtils.isEmpty(baseDomain)) {
setTitle(url);
return;
}
int index = url.indexOf(baseDomain);
if (index == -1) {
setTitle(url);
return;
}
final SpannableStringBuilder builder = new SpannableStringBuilder(url);
builder.setSpan(isPrivate ? mPrivateUrlColorSpan : mUrlColorSpan, 0, url.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
builder.setSpan(isPrivate ? mPrivateDomainColorSpan : mDomainColorSpan,
index, index + baseDomain.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
setTitle(builder);
}
private String stripAboutReaderURL(final String url) {
if (!AboutPages.isAboutReader(url)) {
return url;
}
return ReaderModeUtils.stripAboutReaderUrl(url);
}
private void updateSiteIdentity(@NonNull Tab tab) {
final SiteIdentity siteIdentity = tab.getSiteIdentity();
mSiteIdentityPopup.setSiteIdentity(siteIdentity);
final SecurityMode securityMode;
final MixedMode activeMixedMode;
final MixedMode displayMixedMode;
final TrackingMode trackingMode;
if (siteIdentity == null) {
securityMode = SecurityMode.UNKNOWN;
activeMixedMode = MixedMode.UNKNOWN;
displayMixedMode = MixedMode.UNKNOWN;
trackingMode = TrackingMode.UNKNOWN;
} else {
securityMode = siteIdentity.getSecurityMode();
activeMixedMode = siteIdentity.getMixedModeActive();
displayMixedMode = siteIdentity.getMixedModeDisplay();
trackingMode = siteIdentity.getTrackingMode();
}
// This is a bit tricky, but we have one icon and three potential indicators.
// Default to the identity level
int imageLevel = securityMode.ordinal();
// about: pages should default to having no icon too (the same as SecurityMode.UNKNOWN), however
// SecurityMode.CHROMEUI has a different ordinal - hence we need to manually reset it here.
// (We then continue and process the tracking / mixed content icons as usual, even for about: pages, as they
// can still load external sites.)
if (securityMode == SecurityMode.CHROMEUI) {
imageLevel = LEVEL_DEFAULT_GLOBE; // == SecurityMode.UNKNOWN.ordinal()
}
// Check to see if any protection was overridden first
if (AboutPages.isTitlelessAboutPage(tab.getURL())) {
// We always want to just show a search icon on about:home
imageLevel = LEVEL_SEARCH_ICON;
} else if (trackingMode == TrackingMode.TRACKING_CONTENT_LOADED) {
imageLevel = LEVEL_SHIELD_DISABLED;
} else if (trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED) {
imageLevel = LEVEL_SHIELD_ENABLED;
} else if (activeMixedMode == MixedMode.MIXED_CONTENT_LOADED) {
imageLevel = LEVEL_LOCK_DISABLED;
} else if (displayMixedMode == MixedMode.MIXED_CONTENT_LOADED) {
imageLevel = LEVEL_WARNING_MINOR;
}
if (mSecurityImageLevel != imageLevel) {
mSecurityImageLevel = imageLevel;
mSiteSecurity.setImageLevel(mSecurityImageLevel);
updatePageActions();
}
mTrackingProtectionEnabled = trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED;
}
private void updateProgress(@NonNull Tab tab) {
final boolean shouldShowThrobber = tab.getState() == Tab.STATE_LOADING;
updateUiMode(shouldShowThrobber ? UIMode.PROGRESS : UIMode.DISPLAY);
if (Tab.STATE_SUCCESS == tab.getState() && mTrackingProtectionEnabled) {
mActivity.showTrackingProtectionPromptIfApplicable();
}
}
private void updateUiMode(UIMode uiMode) {
if (mUiMode == uiMode) {
return;
}
mUiMode = uiMode;
// The "Throbber start" and "Throbber stop" log messages in this method
// are needed by S1/S2 tests (http://mrcote.info/phonedash/#).
// See discussion in Bug 804457. Bug 805124 tracks paring these down.
if (mUiMode == UIMode.PROGRESS) {
Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber start");
} else {
Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber stop");
}
updatePageActions();
}
private void updatePageActions() {
final boolean isShowingProgress = (mUiMode == UIMode.PROGRESS);
mStop.setVisibility(isShowingProgress ? View.VISIBLE : View.GONE);
mPageActionLayout.setVisibility(!isShowingProgress ? View.VISIBLE : View.GONE);
// We want title to fill the whole space available for it when there are icons
// being shown on the right side of the toolbar as the icons already have some
// padding in them. This is just to avoid wasting space when icons are shown.
mTitle.setPadding(0, 0, (!isShowingProgress ? mTitlePadding : 0), 0);
}
List<View> getFocusOrder() {
return Arrays.asList(mSiteSecurity, mPageActionLayout, mStop);
}
void setOnStopListener(OnStopListener listener) {
mStopListener = listener;
}
void setOnTitleChangeListener(OnTitleChangeListener listener) {
mTitleChangeListener = listener;
}
/**
* Update the Site Identity popup anchor.
*
* Tablet UI has a tablet-specific doorhanger anchor, so update it after all the views
* are inflated.
* @param view View to use as the anchor for the Site Identity popup.
*/
void updateSiteIdentityAnchor(View view) {
mSiteIdentityPopup.setAnchor(view);
}
void prepareForwardAnimation(PropertyAnimator anim, ForwardButtonAnimation animation, int width) {
if (animation == ForwardButtonAnimation.HIDE) {
// We animate these items individually, rather than this entire view,
// so that we don't animate certain views, e.g. the stop button.
anim.attach(mTitle,
PropertyAnimator.Property.TRANSLATION_X,
0);
anim.attach(mSiteSecurity,
PropertyAnimator.Property.TRANSLATION_X,
0);
// We're hiding the forward button. We're going to reset the margin before
// the animation starts, so we shift these items to the right so that they don't
// appear to move initially.
ViewHelper.setTranslationX(mTitle, width);
ViewHelper.setTranslationX(mSiteSecurity, width);
} else {
anim.attach(mTitle,
PropertyAnimator.Property.TRANSLATION_X,
width);
anim.attach(mSiteSecurity,
PropertyAnimator.Property.TRANSLATION_X,
width);
}
}
void finishForwardAnimation() {
ViewHelper.setTranslationX(mTitle, 0);
ViewHelper.setTranslationX(mSiteSecurity, 0);
}
void prepareStartEditingAnimation() {
// Hide page actions/stop buttons immediately
ViewHelper.setAlpha(mPageActionLayout, 0);
ViewHelper.setAlpha(mStop, 0);
}
void prepareStopEditingAnimation(PropertyAnimator anim) {
// Fade toolbar buttons (page actions, stop) after the entry
// is shrunk back to its original size.
anim.attach(mPageActionLayout,
PropertyAnimator.Property.ALPHA,
1);
anim.attach(mStop,
PropertyAnimator.Property.ALPHA,
1);
}
boolean dismissSiteIdentityPopup() {
if (mSiteIdentityPopup != null && mSiteIdentityPopup.isShowing()) {
mSiteIdentityPopup.dismiss();
return true;
}
return false;
}
void destroy() {
mSiteIdentityPopup.destroy();
}
}

View file

@ -0,0 +1,348 @@
/* -*- 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 android.app.Activity;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.speech.RecognizerIntent;
import android.widget.Button;
import android.widget.ImageButton;
import org.mozilla.gecko.ActivityHandlerHelper;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.R;
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.preferences.GeckoPreferences;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
import org.mozilla.gecko.util.ActivityResultHandler;
import org.mozilla.gecko.util.DrawableUtil;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.StringUtils;
import org.mozilla.gecko.util.InputOptionsUtils;
import org.mozilla.gecko.widget.themed.ThemedLinearLayout;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageView;
import java.util.List;
/**
* {@code ToolbarEditLayout} is the UI for when the toolbar is in
* edit state. It controls a text entry ({@code ToolbarEditText})
* and its matching 'go' button which changes depending on the
* current type of text in the entry.
*/
public class ToolbarEditLayout extends ThemedLinearLayout {
public interface OnSearchStateChangeListener {
public void onSearchStateChange(boolean isActive);
}
private final ImageView mSearchIcon;
private final ToolbarEditText mEditText;
private final ImageButton mVoiceInput;
private final ImageButton mQrCode;
private OnFocusChangeListener mFocusChangeListener;
private boolean showKeyboardOnFocus = false; // Indicates if we need to show the keyboard after the app resumes
public ToolbarEditLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setOrientation(HORIZONTAL);
LayoutInflater.from(context).inflate(R.layout.toolbar_edit_layout, this);
mSearchIcon = (ImageView) findViewById(R.id.search_icon);
mEditText = (ToolbarEditText) findViewById(R.id.url_edit_text);
mVoiceInput = (ImageButton) findViewById(R.id.mic);
mQrCode = (ImageButton) findViewById(R.id.qrcode);
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (HardwareUtils.isTablet()) {
mSearchIcon.setVisibility(View.VISIBLE);
}
mEditText.setOnFocusChangeListener(new OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (mFocusChangeListener != null) {
mFocusChangeListener.onFocusChange(ToolbarEditLayout.this, hasFocus);
// Checking if voice and QR code input are enabled each time the user taps on the URL bar
if (hasFocus) {
if (voiceIsEnabled(getContext(), getResources().getString(R.string.voicesearch_prompt))) {
mVoiceInput.setVisibility(View.VISIBLE);
} else {
mVoiceInput.setVisibility(View.GONE);
}
if (qrCodeIsEnabled(getContext())) {
mQrCode.setVisibility(View.VISIBLE);
} else {
mQrCode.setVisibility(View.GONE);
}
}
}
}
});
mEditText.setOnSearchStateChangeListener(new OnSearchStateChangeListener() {
@Override
public void onSearchStateChange(boolean isActive) {
updateSearchIcon(isActive);
}
});
mVoiceInput.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View v) {
launchVoiceRecognizer();
}
});
mQrCode.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View v) {
launchQRCodeReader();
}
});
// Set an inactive search icon on tablet devices when in editing mode
updateSearchIcon(false);
}
/**
* Update the search icon at the left of the edittext based
* on its state.
*
* @param isActive The state of the edittext. Active is when the initialized
* text has changed and is not empty.
*/
void updateSearchIcon(boolean isActive) {
if (!HardwareUtils.isTablet()) {
return;
}
// When on tablet show a magnifying glass in editing mode
final int searchDrawableId = R.drawable.search_icon_active;
final Drawable searchDrawable;
if (!isActive) {
searchDrawable = DrawableUtil.tintDrawableWithColorRes(getContext(), searchDrawableId, R.color.placeholder_grey);
} else {
if (isPrivateMode()) {
searchDrawable = DrawableUtil.tintDrawableWithColorRes(getContext(), searchDrawableId, R.color.tabs_tray_icon_grey);
} else {
searchDrawable = getResources().getDrawable(searchDrawableId);
}
}
mSearchIcon.setImageDrawable(searchDrawable);
}
@Override
public void setOnFocusChangeListener(OnFocusChangeListener listener) {
mFocusChangeListener = listener;
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
mEditText.setEnabled(enabled);
}
@Override
public void setPrivateMode(boolean isPrivate) {
super.setPrivateMode(isPrivate);
mEditText.setPrivateMode(isPrivate);
}
/**
* Called when the parent gains focus (on app launch and resume)
*/
public void onParentFocus() {
if (showKeyboardOnFocus) {
showKeyboardOnFocus = false;
Activity activity = GeckoAppShell.getGeckoInterface().getActivity();
activity.runOnUiThread(new Runnable() {
public void run() {
mEditText.requestFocus();
showSoftInput();
}
});
}
// Checking if qr code is supported after resuming the app
if (qrCodeIsEnabled(getContext())) {
mQrCode.setVisibility(View.VISIBLE);
} else {
mQrCode.setVisibility(View.GONE);
}
}
void setToolbarPrefs(final ToolbarPrefs prefs) {
mEditText.setToolbarPrefs(prefs);
}
private void showSoftInput() {
InputMethodManager imm =
(InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(mEditText, InputMethodManager.SHOW_IMPLICIT);
}
void prepareShowAnimation(final PropertyAnimator animator) {
if (animator == null) {
mEditText.requestFocus();
showSoftInput();
return;
}
animator.addPropertyAnimationListener(new PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() {
mEditText.requestFocus();
}
@Override
public void onPropertyAnimationEnd() {
showSoftInput();
}
});
}
void setOnCommitListener(OnCommitListener listener) {
mEditText.setOnCommitListener(listener);
}
void setOnDismissListener(OnDismissListener listener) {
mEditText.setOnDismissListener(listener);
}
void setOnFilterListener(OnFilterListener listener) {
mEditText.setOnFilterListener(listener);
}
void onEditSuggestion(String suggestion) {
mEditText.setText(suggestion);
mEditText.setSelection(mEditText.getText().length());
mEditText.requestFocus();
showSoftInput();
}
void setText(String text) {
mEditText.setText(text);
}
String getText() {
return mEditText.getText().toString();
}
protected void saveTabEditingState(final TabEditingState editingState) {
editingState.lastEditingText = mEditText.getNonAutocompleteText();
editingState.selectionStart = mEditText.getSelectionStart();
editingState.selectionEnd = mEditText.getSelectionEnd();
}
protected void restoreTabEditingState(final TabEditingState editingState) {
mEditText.setText(editingState.lastEditingText);
mEditText.setSelection(editingState.selectionStart, editingState.selectionEnd);
}
private boolean voiceIsEnabled(Context context, String prompt) {
final boolean voiceIsSupported = InputOptionsUtils.supportsVoiceRecognizer(context, prompt);
if (!voiceIsSupported) {
return false;
}
return GeckoSharedPrefs.forApp(context)
.getBoolean(GeckoPreferences.PREFS_VOICE_INPUT_ENABLED, true);
}
private void launchVoiceRecognizer() {
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "voice_input_launch");
final Intent intent = InputOptionsUtils.createVoiceRecognizerIntent(getResources().getString(R.string.voicesearch_prompt));
Activity activity = GeckoAppShell.getGeckoInterface().getActivity();
ActivityHandlerHelper.startIntentForActivity(activity, intent, new ActivityResultHandler() {
@Override
public void onActivityResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return;
}
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "voice_input_success");
// We have RESULT_OK, not RESULT_NO_MATCH so it should be safe to assume that
// we have at least one match. We only need one: this will be
// used for showing the user search engines with this search term in it.
List<String> voiceStrings = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
String text = voiceStrings.get(0);
mEditText.setText(text);
mEditText.setSelection(0, text.length());
final InputMethodManager imm =
(InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(mEditText, InputMethodManager.SHOW_IMPLICIT);
}
});
}
private boolean qrCodeIsEnabled(Context context) {
final boolean qrCodeIsSupported = InputOptionsUtils.supportsQrCodeReader(context);
if (!qrCodeIsSupported) {
return false;
}
return GeckoSharedPrefs.forApp(context)
.getBoolean(GeckoPreferences.PREFS_QRCODE_ENABLED, true);
}
private void launchQRCodeReader() {
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "qrcode_input_launch");
final Intent intent = InputOptionsUtils.createQRCodeReaderIntent();
Activity activity = GeckoAppShell.getGeckoInterface().getActivity();
ActivityHandlerHelper.startIntentForActivity(activity, intent, new ActivityResultHandler() {
@Override
public void onActivityResult(int resultCode, Intent intent) {
if (resultCode == Activity.RESULT_OK) {
String text = intent.getStringExtra("SCAN_RESULT");
if (!StringUtils.isSearchQuery(text, false)) {
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "qrcode_input_success");
mEditText.setText(text);
mEditText.selectAll();
// Queuing up the keyboard show action.
// At this point the app has not resumed yet, and trying to show
// the keyboard will fail.
showKeyboardOnFocus = true;
}
}
// We can get the SCAN_RESULT_FORMAT, SCAN_RESULT_BYTES,
// SCAN_RESULT_ORIENTATION and SCAN_RESULT_ERROR_CORRECTION_LEVEL
// as well as the actual result, which may hold a URL.
}
});
}
}

View file

@ -0,0 +1,630 @@
/* -*- 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 org.mozilla.gecko.AboutPages;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.CustomEditText;
import org.mozilla.gecko.InputMethods;
import org.mozilla.gecko.R;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener;
import org.mozilla.gecko.toolbar.ToolbarEditLayout.OnSearchStateChangeListener;
import org.mozilla.gecko.util.GamepadUtils;
import org.mozilla.gecko.util.StringUtils;
import android.content.Context;
import android.graphics.Rect;
import android.text.Editable;
import android.text.NoCopySpan;
import android.text.Selection;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.BackgroundColorSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputConnectionWrapper;
import android.view.inputmethod.InputMethodManager;
import android.view.accessibility.AccessibilityEvent;
import android.widget.TextView;
/**
* {@code ToolbarEditText} is the text entry used when the toolbar
* is in edit state. It handles all the necessary input method machinery.
* It's meant to be owned by {@code ToolbarEditLayout}.
*/
public class ToolbarEditText extends CustomEditText
implements AutocompleteHandler {
private static final String LOGTAG = "GeckoToolbarEditText";
private static final NoCopySpan AUTOCOMPLETE_SPAN = new NoCopySpan.Concrete();
private final Context mContext;
private OnCommitListener mCommitListener;
private OnDismissListener mDismissListener;
private OnFilterListener mFilterListener;
private OnSearchStateChangeListener mSearchStateChangeListener;
private ToolbarPrefs mPrefs;
// The previous autocomplete result returned to us
private String mAutoCompleteResult = "";
// Length of the user-typed portion of the result
private int mAutoCompletePrefixLength;
// If text change is due to us setting autocomplete
private boolean mSettingAutoComplete;
// Spans used for marking the autocomplete text
private Object[] mAutoCompleteSpans;
// Do not process autocomplete result
private boolean mDiscardAutoCompleteResult;
public ToolbarEditText(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
}
void setOnCommitListener(OnCommitListener listener) {
mCommitListener = listener;
}
void setOnDismissListener(OnDismissListener listener) {
mDismissListener = listener;
}
void setOnFilterListener(OnFilterListener listener) {
mFilterListener = listener;
}
void setOnSearchStateChangeListener(OnSearchStateChangeListener listener) {
mSearchStateChangeListener = listener;
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
setOnKeyListener(new KeyListener());
setOnKeyPreImeListener(new KeyPreImeListener());
setOnSelectionChangedListener(new SelectionChangeListener());
addTextChangedListener(new TextChangeListener());
}
@Override
public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
// Make search icon inactive when edit toolbar search term isn't a user entered
// search term
final boolean isActive = !TextUtils.isEmpty(getText());
if (mSearchStateChangeListener != null) {
mSearchStateChangeListener.onSearchStateChange(isActive);
}
if (gainFocus) {
resetAutocompleteState();
return;
}
removeAutocomplete(getText());
final InputMethodManager imm = InputMethods.getInputMethodManager(mContext);
try {
imm.restartInput(this);
imm.hideSoftInputFromWindow(getWindowToken(), 0);
} catch (NullPointerException e) {
Log.e(LOGTAG, "InputMethodManagerService, why are you throwing"
+ " a NullPointerException? See bug 782096", e);
}
}
@Override
public void setText(final CharSequence text, final TextView.BufferType type) {
final String textString = (text == null) ? "" : text.toString();
// If we're on the home or private browsing page, we don't set the "about" url.
final CharSequence finalText;
if (AboutPages.isAboutHome(textString) || AboutPages.isAboutPrivateBrowsing(textString)) {
finalText = "";
} else {
finalText = text;
}
super.setText(finalText, type);
// Any autocomplete text would have been overwritten, so reset our autocomplete states.
resetAutocompleteState();
}
@Override
public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
// We need to bypass the isShown() check in the default implementation
// for TYPE_VIEW_TEXT_SELECTION_CHANGED events so that accessibility
// services could detect a url change.
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED &&
getParent() != null && !isShown()) {
onInitializeAccessibilityEvent(event);
dispatchPopulateAccessibilityEvent(event);
getParent().requestSendAccessibilityEvent(this, event);
} else {
super.sendAccessibilityEventUnchecked(event);
}
}
void setToolbarPrefs(final ToolbarPrefs prefs) {
mPrefs = prefs;
}
/**
* Mark the start of autocomplete changes so our text change
* listener does not react to changes in autocomplete text
*/
private void beginSettingAutocomplete() {
beginBatchEdit();
mSettingAutoComplete = true;
}
/**
* Mark the end of autocomplete changes
*/
private void endSettingAutocomplete() {
mSettingAutoComplete = false;
endBatchEdit();
}
/**
* Reset autocomplete states to their initial values
*/
private void resetAutocompleteState() {
mAutoCompleteSpans = new Object[] {
// Span to mark the autocomplete text
AUTOCOMPLETE_SPAN,
// Span to change the autocomplete text color
new BackgroundColorSpan(getHighlightColor())
};
mAutoCompleteResult = "";
// Pretend we already autocompleted the existing text,
// so that actions like backspacing don't trigger autocompletion.
mAutoCompletePrefixLength = getText().length();
// Show the cursor.
setCursorVisible(true);
}
protected String getNonAutocompleteText() {
return getNonAutocompleteText(getText());
}
/**
* Get the portion of text that is not marked as autocomplete text.
*
* @param text Current text content that may include autocomplete text
*/
private static String getNonAutocompleteText(final Editable text) {
final int start = text.getSpanStart(AUTOCOMPLETE_SPAN);
if (start < 0) {
// No autocomplete text; return the whole string.
return text.toString();
}
// Only return the portion that's not autocomplete text
return TextUtils.substring(text, 0, start);
}
/**
* Remove any autocomplete text
*
* @param text Current text content that may include autocomplete text
*/
private boolean removeAutocomplete(final Editable text) {
final int start = text.getSpanStart(AUTOCOMPLETE_SPAN);
if (start < 0) {
// No autocomplete text
return false;
}
beginSettingAutocomplete();
// When we call delete() here, the autocomplete spans we set are removed as well.
text.delete(start, text.length());
// Keep mAutoCompletePrefixLength the same because the prefix has not changed.
// Clear mAutoCompleteResult to make sure we get fresh autocomplete text next time.
mAutoCompleteResult = "";
// Reshow the cursor.
setCursorVisible(true);
endSettingAutocomplete();
return true;
}
/**
* Convert any autocomplete text to regular text
*
* @param text Current text content that may include autocomplete text
*/
private boolean commitAutocomplete(final Editable text) {
final int start = text.getSpanStart(AUTOCOMPLETE_SPAN);
if (start < 0) {
// No autocomplete text
return false;
}
beginSettingAutocomplete();
// Remove all spans here to convert from autocomplete text to regular text
for (final Object span : mAutoCompleteSpans) {
text.removeSpan(span);
}
// Keep mAutoCompleteResult the same because the result has not changed.
// Reset mAutoCompletePrefixLength because the prefix now includes the autocomplete text.
mAutoCompletePrefixLength = text.length();
// Reshow the cursor.
setCursorVisible(true);
endSettingAutocomplete();
// Filter on the new text
if (mFilterListener != null) {
mFilterListener.onFilter(text.toString(), null);
}
return true;
}
/**
* Add autocomplete text based on the result URI.
*
* @param result Result URI to be turned into autocomplete text
*/
@Override
public final void onAutocomplete(final String result) {
// If mDiscardAutoCompleteResult is true, we temporarily disabled
// autocomplete (due to backspacing, etc.) and we should bail early.
if (mDiscardAutoCompleteResult) {
return;
}
if (!isEnabled() || result == null) {
mAutoCompleteResult = "";
return;
}
final Editable text = getText();
final int textLength = text.length();
final int resultLength = result.length();
final int autoCompleteStart = text.getSpanStart(AUTOCOMPLETE_SPAN);
mAutoCompleteResult = result;
if (autoCompleteStart > -1) {
// Autocomplete text already exists; we should replace existing autocomplete text.
// If the result and the current text don't have the same prefixes,
// the result is stale and we should wait for the another result to come in.
if (!TextUtils.regionMatches(result, 0, text, 0, autoCompleteStart)) {
return;
}
beginSettingAutocomplete();
// Replace the existing autocomplete text with new one.
// replace() preserves the autocomplete spans that we set before.
text.replace(autoCompleteStart, textLength, result, autoCompleteStart, resultLength);
// Reshow the cursor if there is no longer any autocomplete text.
if (autoCompleteStart == resultLength) {
setCursorVisible(true);
}
endSettingAutocomplete();
} else {
// No autocomplete text yet; we should add autocomplete text
// If the result prefix doesn't match the current text,
// the result is stale and we should wait for the another result to come in.
if (resultLength <= textLength ||
!TextUtils.regionMatches(result, 0, text, 0, textLength)) {
return;
}
final Object[] spans = text.getSpans(textLength, textLength, Object.class);
final int[] spanStarts = new int[spans.length];
final int[] spanEnds = new int[spans.length];
final int[] spanFlags = new int[spans.length];
// Save selection/composing span bounds so we can restore them later.
for (int i = 0; i < spans.length; i++) {
final Object span = spans[i];
final int spanFlag = text.getSpanFlags(span);
// We don't care about spans that are not selection or composing spans.
// For those spans, spanFlag[i] will be 0 and we don't restore them.
if ((spanFlag & Spanned.SPAN_COMPOSING) == 0 &&
(span != Selection.SELECTION_START) &&
(span != Selection.SELECTION_END)) {
continue;
}
spanStarts[i] = text.getSpanStart(span);
spanEnds[i] = text.getSpanEnd(span);
spanFlags[i] = spanFlag;
}
beginSettingAutocomplete();
// First add trailing text.
text.append(result, textLength, resultLength);
// Restore selection/composing spans.
for (int i = 0; i < spans.length; i++) {
final int spanFlag = spanFlags[i];
if (spanFlag == 0) {
// Skip if the span was ignored before.
continue;
}
text.setSpan(spans[i], spanStarts[i], spanEnds[i], spanFlag);
}
// Mark added text as autocomplete text.
for (final Object span : mAutoCompleteSpans) {
text.setSpan(span, textLength, resultLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
// Hide the cursor.
setCursorVisible(false);
// Make sure the autocomplete text is visible. If the autocomplete text is too
// long, it would appear the cursor will be scrolled out of view. However, this
// is not the case in practice, because EditText still makes sure the cursor is
// still in view.
bringPointIntoView(resultLength);
endSettingAutocomplete();
}
}
private static boolean hasCompositionString(Editable content) {
Object[] spans = content.getSpans(0, content.length(), Object.class);
if (spans != null) {
for (Object span : spans) {
if ((content.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) {
// Found composition string.
return true;
}
}
}
return false;
}
/**
* Code to handle deleting autocomplete first when backspacing.
* If there is no autocomplete text, both removeAutocomplete() and commitAutocomplete()
* are no-ops and return false. Therefore we can use them here without checking explicitly
* if we have autocomplete text or not.
*/
@Override
public InputConnection onCreateInputConnection(final EditorInfo outAttrs) {
final InputConnection ic = super.onCreateInputConnection(outAttrs);
if (ic == null) {
return null;
}
return new InputConnectionWrapper(ic, false) {
@Override
public boolean deleteSurroundingText(final int beforeLength, final int afterLength) {
if (removeAutocomplete(getText())) {
// If we have autocomplete text, the cursor is at the boundary between
// regular and autocomplete text. So regardless of which direction we
// are deleting, we should delete the autocomplete text first.
// Make the IME aware that we interrupted the deleteSurroundingText call,
// by restarting the IME.
final InputMethodManager imm = InputMethods.getInputMethodManager(mContext);
if (imm != null) {
imm.restartInput(ToolbarEditText.this);
}
return false;
}
return super.deleteSurroundingText(beforeLength, afterLength);
}
private boolean removeAutocompleteOnComposing(final CharSequence text) {
final Editable editable = getText();
final int composingStart = BaseInputConnection.getComposingSpanStart(editable);
final int composingEnd = BaseInputConnection.getComposingSpanEnd(editable);
// We only delete the autocomplete text when the user is backspacing,
// i.e. when the composing text is getting shorter.
if (composingStart >= 0 &&
composingEnd >= 0 &&
(composingEnd - composingStart) > text.length() &&
removeAutocomplete(editable)) {
// Make the IME aware that we interrupted the setComposingText call,
// by having finishComposingText() send change notifications to the IME.
finishComposingText();
setComposingRegion(composingStart, composingEnd);
return true;
}
return false;
}
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
if (removeAutocompleteOnComposing(text)) {
return false;
}
return super.commitText(text, newCursorPosition);
}
@Override
public boolean setComposingText(final CharSequence text, final int newCursorPosition) {
if (removeAutocompleteOnComposing(text)) {
return false;
}
return super.setComposingText(text, newCursorPosition);
}
};
}
private class SelectionChangeListener implements OnSelectionChangedListener {
@Override
public void onSelectionChanged(final int selStart, final int selEnd) {
// The user has repositioned the cursor somewhere. We need to adjust
// the autocomplete text depending on where the new cursor is.
final Editable text = getText();
final int start = text.getSpanStart(AUTOCOMPLETE_SPAN);
if (mSettingAutoComplete || start < 0 || (start == selStart && start == selEnd)) {
// Do not commit autocomplete text if there is no autocomplete text
// or if selection is still at start of autocomplete text
return;
}
if (selStart <= start && selEnd <= start) {
// The cursor is in user-typed text; remove any autocomplete text.
removeAutocomplete(text);
} else {
// The cursor is in the autocomplete text; commit it so it becomes regular text.
commitAutocomplete(text);
}
}
}
private class TextChangeListener implements TextWatcher {
@Override
public void afterTextChanged(final Editable editable) {
if (!isEnabled() || mSettingAutoComplete) {
return;
}
final String text = getNonAutocompleteText(editable);
final int textLength = text.length();
boolean doAutocomplete = mPrefs.shouldAutocomplete();
if (StringUtils.isSearchQuery(text, false)) {
doAutocomplete = false;
} else if (mAutoCompletePrefixLength > textLength) {
// If you're hitting backspace (the string is getting smaller), don't autocomplete
doAutocomplete = false;
}
mAutoCompletePrefixLength = textLength;
// If we are not autocompleting, we set mDiscardAutoCompleteResult to true
// to discard any autocomplete results that are in-flight, and vice versa.
mDiscardAutoCompleteResult = !doAutocomplete;
if (doAutocomplete && mAutoCompleteResult.startsWith(text)) {
// If this text already matches our autocomplete text, autocomplete likely
// won't change. Just reuse the old autocomplete value.
onAutocomplete(mAutoCompleteResult);
doAutocomplete = false;
} else {
// Otherwise, remove the old autocomplete text
// until any new autocomplete text gets added.
removeAutocomplete(editable);
}
// Update search icon with an active state since user is typing
if (mSearchStateChangeListener != null) {
mSearchStateChangeListener.onSearchStateChange(textLength > 0);
}
if (mFilterListener != null) {
mFilterListener.onFilter(text, doAutocomplete ? ToolbarEditText.this : null);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// do nothing
}
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
// do nothing
}
}
private class KeyPreImeListener implements OnKeyPreImeListener {
@Override
public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) {
// We only want to process one event per tap
if (event.getAction() != KeyEvent.ACTION_DOWN) {
return false;
}
if (keyCode == KeyEvent.KEYCODE_ENTER) {
// If the edit text has a composition string, don't submit the text yet.
// ENTER is needed to commit the composition string.
final Editable content = getText();
if (!hasCompositionString(content)) {
if (mCommitListener != null) {
mCommitListener.onCommit();
}
return true;
}
}
if (keyCode == KeyEvent.KEYCODE_BACK) {
// Drop the virtual keyboard.
clearFocus();
return true;
}
return false;
}
}
private class KeyListener implements View.OnKeyListener {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_ENTER || GamepadUtils.isActionKey(event)) {
if (event.getAction() != KeyEvent.ACTION_DOWN) {
return true;
}
if (mCommitListener != null) {
mCommitListener.onCommit();
}
return true;
}
if (GamepadUtils.isBackKey(event)) {
if (mDismissListener != null) {
mDismissListener.onDismiss();
}
return true;
}
if ((keyCode == KeyEvent.KEYCODE_DEL ||
(keyCode == KeyEvent.KEYCODE_FORWARD_DEL)) &&
removeAutocomplete(getText())) {
// Delete autocomplete text when backspacing or forward deleting.
return true;
}
return false;
}
}
}

View file

@ -0,0 +1,78 @@
/* -*- 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 org.mozilla.gecko.PrefsHelper;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.util.ThreadUtils;
class ToolbarPrefs {
private static final String PREF_AUTOCOMPLETE_ENABLED = "browser.urlbar.autocomplete.enabled";
private static final String PREF_TRIM_URLS = "browser.urlbar.trimURLs";
private static final String[] PREFS = {
PREF_AUTOCOMPLETE_ENABLED,
PREF_TRIM_URLS
};
private final TitlePrefsHandler HANDLER = new TitlePrefsHandler();
private volatile boolean enableAutocomplete;
private volatile boolean trimUrls;
ToolbarPrefs() {
// Skip autocompletion while Gecko is loading.
// We will get the correct pref value once Gecko is loaded.
enableAutocomplete = false;
trimUrls = true;
}
boolean shouldAutocomplete() {
return enableAutocomplete;
}
boolean shouldTrimUrls() {
return trimUrls;
}
void open() {
PrefsHelper.addObserver(PREFS, HANDLER);
}
void close() {
PrefsHelper.removeObserver(HANDLER);
}
private void triggerTitleChangeListener() {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
final Tabs tabs = Tabs.getInstance();
final Tab tab = tabs.getSelectedTab();
if (tab != null) {
tabs.notifyListeners(tab, Tabs.TabEvents.TITLE);
}
}
});
}
private class TitlePrefsHandler extends PrefsHelper.PrefHandlerBase {
@Override
public void prefValue(String pref, boolean value) {
if (PREF_AUTOCOMPLETE_ENABLED.equals(pref)) {
enableAutocomplete = value;
} else if (PREF_TRIM_URLS.equals(pref)) {
// Handles PREF_TRIM_URLS, which should usually be a boolean.
if (value != trimUrls) {
trimUrls = value;
triggerTitleChangeListener();
}
}
}
}
}

View file

@ -0,0 +1,195 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mozilla.gecko.toolbar;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.R;
import org.mozilla.gecko.widget.themed.ThemedImageView;
import org.mozilla.gecko.util.WeakReferenceHandler;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
/**
* Progress view used for page loads.
*
* Because we're given limited information about the page load progress, the
* bar also includes incremental animation between each step to improve
* perceived performance.
*/
public class ToolbarProgressView extends ThemedImageView {
private static final int MAX_PROGRESS = 10000;
private static final int MSG_UPDATE = 0;
private static final int MSG_HIDE = 1;
private static final int STEPS = 10;
private static final int DELAY = 40;
private int mTargetProgress;
private int mIncrement;
private Rect mBounds;
private Handler mHandler;
private int mCurrentProgress;
private PorterDuffColorFilter mPrivateBrowsingColorFilter;
public ToolbarProgressView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
public ToolbarProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context ctx) {
mBounds = new Rect(0, 0, 0, 0);
mTargetProgress = 0;
mPrivateBrowsingColorFilter = new PorterDuffColorFilter(
ContextCompat.getColor(ctx, R.color.private_browsing_purple), PorterDuff.Mode.SRC_IN);
mHandler = new ToolbarProgressHandler(this);
}
@Override
public void onLayout(boolean f, int l, int t, int r, int b) {
mBounds.left = 0;
mBounds.right = (r - l) * mCurrentProgress / MAX_PROGRESS;
mBounds.top = 0;
mBounds.bottom = b - t;
}
@Override
public void onDraw(Canvas canvas) {
final Drawable d = getDrawable();
d.setBounds(mBounds);
d.draw(canvas);
}
/**
* Immediately sets the progress bar to the given progress percentage.
*
* @param progress Percentage (0-100) to which progress bar should be set
*/
void setProgress(int progressPercentage) {
mCurrentProgress = mTargetProgress = getAbsoluteProgress(progressPercentage);
updateBounds();
clearMessages();
}
/**
* Animates the progress bar from the current progress value to the given
* progress percentage.
*
* @param progress Percentage (0-100) to which progress bar should be animated
*/
void animateProgress(int progressPercentage) {
final int absoluteProgress = getAbsoluteProgress(progressPercentage);
if (absoluteProgress <= mTargetProgress) {
// After we manually click stop, we can still receive page load
// events (e.g., DOMContentLoaded). Updating for other updates
// after a STOP event can freeze the progress bar, so guard against
// that here.
return;
}
mTargetProgress = absoluteProgress;
mIncrement = (mTargetProgress - mCurrentProgress) / STEPS;
clearMessages();
mHandler.sendEmptyMessage(MSG_UPDATE);
}
private void clearMessages() {
mHandler.removeMessages(MSG_UPDATE);
mHandler.removeMessages(MSG_HIDE);
}
private int getAbsoluteProgress(int progressPercentage) {
if (progressPercentage < 0) {
return 0;
}
if (progressPercentage > 100) {
return 100;
}
return progressPercentage * MAX_PROGRESS / 100;
}
private void updateBounds() {
mBounds.right = getWidth() * mCurrentProgress / MAX_PROGRESS;
invalidate();
}
@Override
public void setPrivateMode(final boolean isPrivate) {
super.setPrivateMode(isPrivate);
// Note: android:tint is better but ColorStateLists are not supported until API 21.
if (isPrivate) {
setColorFilter(mPrivateBrowsingColorFilter);
} else {
clearColorFilter();
}
}
private static class ToolbarProgressHandler extends WeakReferenceHandler<ToolbarProgressView> {
public ToolbarProgressHandler(final ToolbarProgressView that) {
super(that);
}
@Override
public void handleMessage(Message msg) {
final ToolbarProgressView that = mTarget.get();
if (that == null) {
return;
}
switch (msg.what) {
case MSG_UPDATE:
that.mCurrentProgress = Math.min(that.mTargetProgress, that.mCurrentProgress + that.mIncrement);
that.updateBounds();
if (that.mCurrentProgress < that.mTargetProgress) {
final int delay = (that.mTargetProgress < MAX_PROGRESS) ? DELAY : DELAY / 4;
sendMessageDelayed(that.mHandler.obtainMessage(msg.what), delay);
} else if (that.mCurrentProgress == MAX_PROGRESS) {
sendMessageDelayed(that.mHandler.obtainMessage(MSG_HIDE), DELAY);
}
break;
case MSG_HIDE:
that.setVisibility(View.GONE);
break;
}
}
};
}