// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.omnibox;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.text.Selection;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageView;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.WindowDelegate;
import org.chromium.chrome.browser.appmenu.AppMenuButtonHelper;
import org.chromium.chrome.browser.ntp.NewTabPage;
import org.chromium.chrome.browser.omaha.UpdateMenuItemHelper;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.widget.TintedImageButton;
import org.chromium.ui.UiUtils;
/**
* A location bar implementation specific for smaller/phone screens.
*/
public class LocationBarPhone extends LocationBarLayout {
private static final int KEYBOARD_MODE_CHANGE_DELAY_MS = 300;
private static final int KEYBOARD_HIDE_DELAY_MS = 150;
private static final int ACTION_BUTTON_TOUCH_OVERFLOW_LEFT = 15;
private View mFirstVisibleFocusedView;
private View mIncognitoBadge;
private View mUrlActionsContainer;
private TintedImageButton mMenuButton;
private ImageView mMenuBadge;
private View mMenuButtonWrapper;
private int mIncognitoBadgePadding;
private float mUrlFocusChangePercent;
private Runnable mKeyboardResizeModeTask;
private ObjectAnimator mOmniboxBackgroundAnimator;
private boolean mShowMenuBadge;
private AnimatorSet mMenuBadgeAnimatorSet;
private boolean mIsMenuBadgeAnimationRunning;
/**
* Constructor used to inflate from XML.
*/
public LocationBarPhone(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mFirstVisibleFocusedView = findViewById(R.id.url_bar);
mIncognitoBadge = findViewById(R.id.incognito_badge);
mIncognitoBadgePadding =
getResources().getDimensionPixelSize(R.dimen.location_bar_incognito_badge_padding);
mUrlActionsContainer = findViewById(R.id.url_action_container);
Rect delegateArea = new Rect();
mUrlActionsContainer.getHitRect(delegateArea);
delegateArea.left -= ACTION_BUTTON_TOUCH_OVERFLOW_LEFT;
TouchDelegate touchDelegate = new TouchDelegate(delegateArea, mUrlActionsContainer);
assert mUrlActionsContainer.getParent() == this;
setTouchDelegate(touchDelegate);
mMenuButton = (TintedImageButton) findViewById(R.id.document_menu_button);
mMenuBadge = (ImageView) findViewById(R.id.document_menu_badge);
mMenuButtonWrapper = findViewById(R.id.document_menu_button_wrapper);
((ViewGroup) mMenuButtonWrapper.getParent()).removeView(mMenuButtonWrapper);
}
@Override
public void setMenuButtonHelper(final AppMenuButtonHelper helper) {
super.setMenuButtonHelper(helper);
mMenuButton.setOnTouchListener(new OnTouchListener() {
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
return helper.onTouch(v, event);
}
});
mMenuButton.setOnKeyListener(new OnKeyListener() {
@Override
public boolean onKey(View view, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
return helper.onEnterKeyPress(view);
}
return false;
}
});
}
@Override
public View getMenuAnchor() {
return mMenuButton;
}
@Override
public void getContentRect(Rect outRect) {
super.getContentRect(outRect);
if (mIncognitoBadge.getVisibility() == View.GONE) return;
if (!ApiCompatibilityUtils.isLayoutRtl(this)) {
outRect.left += mIncognitoBadge.getWidth();
} else {
outRect.right -= mIncognitoBadge.getWidth();
}
}
/**
* @return The first view visible when the location bar is focused.
*/
public View getFirstViewVisibleWhenFocused() {
return mFirstVisibleFocusedView;
}
/**
* Updates percentage of current the URL focus change animation.
* @param percent 1.0 is 100% focused, 0 is completely unfocused.
*/
public void setUrlFocusChangePercent(float percent) {
mUrlFocusChangePercent = percent;
if (percent > 0f) {
mUrlActionsContainer.setVisibility(VISIBLE);
} else if (percent == 0f && !isUrlFocusChangeInProgress()) {
// If a URL focus change is in progress, then it will handle setting the visibility
// correctly after it completes. If done here, it would cause the URL to jump due
// to a badly timed layout call.
mUrlActionsContainer.setVisibility(GONE);
}
updateButtonVisibility();
}
@Override
public void onUrlFocusChange(boolean hasFocus) {
if (mOmniboxBackgroundAnimator != null && mOmniboxBackgroundAnimator.isRunning()) {
mOmniboxBackgroundAnimator.cancel();
mOmniboxBackgroundAnimator = null;
}
if (hasFocus) {
// Remove the focus of this view once the URL field has taken focus as this view no
// longer needs it.
setFocusable(false);
setFocusableInTouchMode(false);
}
setUrlFocusChangeInProgress(true);
super.onUrlFocusChange(hasFocus);
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
boolean needsCanvasRestore = false;
if (child == mUrlBar && mUrlActionsContainer.getVisibility() == VISIBLE) {
canvas.save();
// Clip the URL bar contents to ensure they do not draw under the URL actions during
// focus animations. Based on the RTL state of the location bar, the url actions
// container can be on the left or right side, so clip accordingly.
if (mUrlBar.getLeft() < mUrlActionsContainer.getLeft()) {
canvas.clipRect(0, 0, (int) mUrlActionsContainer.getX(), getBottom());
} else {
canvas.clipRect(mUrlActionsContainer.getX() + mUrlActionsContainer.getWidth(),
0, getWidth(), getBottom());
}
needsCanvasRestore = true;
}
boolean retVal = super.drawChild(canvas, child, drawingTime);
if (needsCanvasRestore) {
canvas.restore();
}
return retVal;
}
/**
* Handles any actions to be performed after all other actions triggered by the URL focus
* change. This will be called after any animations are performed to transition from one
* focus state to the other.
* @param hasFocus Whether the URL field has gained focus.
*/
public void finishUrlFocusChange(boolean hasFocus) {
final WindowDelegate windowDelegate = getWindowDelegate();
if (!hasFocus) {
// Remove the selection from the url text. The ending selection position
// will determine the scroll position when the url field is restored. If
// we do not clear this, it will scroll to the end of the text when you
// enter/exit the tab stack.
// We set the selection to 0 instead of removing the selection to avoid a crash that
// happens if you clear the selection instead.
//
// Triggering the bug happens by:
// 1.) Selecting some portion of the URL (where the two selection handles
// appear)
// 2.) Trigger a text change in the URL bar (i.e. by triggering a new URL load
// by a command line intent)
// 3.) Simultaneously moving one of the selection handles left and right. This will
// occasionally throw an AssertionError on the bounds of the selection.
Selection.setSelection(mUrlBar.getText(), 0);
// The animation rendering may not yet be 100% complete and hiding the keyboard makes
// the animation quite choppy.
postDelayed(new Runnable() {
@Override
public void run() {
UiUtils.hideKeyboard(mUrlBar);
}
}, KEYBOARD_HIDE_DELAY_MS);
// Convert the keyboard back to resize mode (delay the change for an arbitrary amount
// of time in hopes the keyboard will be completely hidden before making this change).
if (mKeyboardResizeModeTask == null
&& windowDelegate.getWindowSoftInputMode()
!= WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) {
mKeyboardResizeModeTask = new Runnable() {
@Override
public void run() {
windowDelegate.setWindowSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
mKeyboardResizeModeTask = null;
}
};
postDelayed(mKeyboardResizeModeTask, KEYBOARD_MODE_CHANGE_DELAY_MS);
}
mUrlActionsContainer.setVisibility(GONE);
} else {
if (mKeyboardResizeModeTask != null) {
removeCallbacks(mKeyboardResizeModeTask);
mKeyboardResizeModeTask = null;
}
if (windowDelegate.getWindowSoftInputMode()
!= WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) {
windowDelegate.setWindowSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
}
UiUtils.showKeyboard(mUrlBar);
// As the position of the navigation icon has changed, ensure the suggestions are
// updated to reflect the new position.
if (getSuggestionList() != null && getSuggestionList().isShown()) {
getSuggestionList().invalidateSuggestionViews();
}
}
setUrlFocusChangeInProgress(false);
NewTabPage ntp = getToolbarDataProvider().getNewTabPageForCurrentTab();
if (hasFocus && ntp != null && ntp.isLocationBarShownInNTP()) {
fadeInOmniboxResultsContainerBackground();
}
}
@Override
protected void updateButtonVisibility() {
updateDeleteButtonVisibility();
updateMicButtonVisiblity(mUrlFocusChangePercent);
}
@Override
protected void updateLocationBarIconContainerVisibility() {
super.updateLocationBarIconContainerVisibility();
updateIncognitoBadgePadding();
}
private void updateIncognitoBadgePadding() {
// This can be triggered in the super.onFinishInflate, so we need to null check in this
// place only.
if (mIncognitoBadge == null) return;
if (findViewById(R.id.location_bar_icon).getVisibility() == GONE) {
ApiCompatibilityUtils.setPaddingRelative(
mIncognitoBadge, 0, 0, mIncognitoBadgePadding, 0);
} else {
ApiCompatibilityUtils.setPaddingRelative(mIncognitoBadge, 0, 0, 0, 0);
}
}
@Override
public void updateVisualsForState() {
super.updateVisualsForState();
Tab tab = getCurrentTab();
boolean isIncognito = tab != null && tab.isIncognito();
mIncognitoBadge.setVisibility(isIncognito ? VISIBLE : GONE);
updateIncognitoBadgePadding();
}
@Override
protected boolean shouldAnimateIconChanges() {
return super.shouldAnimateIconChanges() || isUrlFocusChangeInProgress();
}
@Override
public void setLayoutDirection(int layoutDirection) {
super.setLayoutDirection(layoutDirection);
updateIncognitoBadgePadding();
}
/**
* Remove the update menu app menu badge.
*/
public void removeAppMenuUpdateBadge(boolean animate) {
boolean wasShowingMenuBadge = mShowMenuBadge;
mShowMenuBadge = false;
mMenuButton.setContentDescription(getResources().getString(
R.string.accessibility_toolbar_btn_menu));
if (!animate || !wasShowingMenuBadge) return;
if (mIsMenuBadgeAnimationRunning && mMenuBadgeAnimatorSet != null) {
mMenuBadgeAnimatorSet.cancel();
}
// Set initial states.
mMenuButton.setAlpha(0.f);
mMenuBadgeAnimatorSet = UpdateMenuItemHelper.createHideUpdateBadgeAnimation(
mMenuButton, mMenuBadge);
mMenuBadgeAnimatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mIsMenuBadgeAnimationRunning = true;
}
@Override
public void onAnimationEnd(Animator animation) {
mIsMenuBadgeAnimationRunning = false;
}
@Override
public void onAnimationCancel(Animator animation) {
mIsMenuBadgeAnimationRunning = false;
}
});
mMenuBadgeAnimatorSet.start();
}
public void cancelAppMenuUpdateBadgeAnimation() {
if (mIsMenuBadgeAnimationRunning && mMenuBadgeAnimatorSet != null) {
mMenuBadgeAnimatorSet.cancel();
}
}
}