mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-07 00:08:39 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue