michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.toolbar; michael@0: michael@0: import org.mozilla.gecko.AboutPages; michael@0: import org.mozilla.gecko.animation.PropertyAnimator; michael@0: import org.mozilla.gecko.animation.ViewHelper; michael@0: import org.mozilla.gecko.BrowserApp; michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.SiteIdentity; michael@0: import org.mozilla.gecko.SiteIdentity.SecurityMode; michael@0: import org.mozilla.gecko.Tab; michael@0: import org.mozilla.gecko.Tabs; michael@0: import org.mozilla.gecko.toolbar.BrowserToolbar.ForwardButtonAnimation; michael@0: import org.mozilla.gecko.util.StringUtils; michael@0: import org.mozilla.gecko.widget.ThemedLinearLayout; michael@0: import org.mozilla.gecko.widget.ThemedTextView; michael@0: michael@0: import org.json.JSONObject; michael@0: michael@0: import android.content.Context; michael@0: import android.content.res.Resources; michael@0: import android.graphics.Bitmap; michael@0: import android.os.Build; michael@0: import android.os.SystemClock; michael@0: import android.text.style.ForegroundColorSpan; michael@0: import android.text.Spannable; michael@0: import android.text.SpannableStringBuilder; michael@0: import android.text.Spanned; michael@0: import android.text.TextUtils; michael@0: import android.util.AttributeSet; michael@0: import android.util.Log; michael@0: import android.view.LayoutInflater; michael@0: import android.view.View; michael@0: import android.view.animation.Animation; michael@0: import android.view.animation.AnimationUtils; michael@0: import android.view.animation.AlphaAnimation; michael@0: import android.view.animation.TranslateAnimation; michael@0: import android.widget.Button; michael@0: import android.widget.ImageButton; michael@0: import android.widget.LinearLayout.LayoutParams; michael@0: michael@0: import java.util.Arrays; michael@0: import java.util.EnumSet; michael@0: import java.util.List; michael@0: michael@0: /** michael@0: * {@code ToolbarDisplayLayout} is the UI for when the toolbar is in michael@0: * display state. It's used to display the state of the currently selected michael@0: * tab. It should always be updated through a single entry point michael@0: * (updateFromTab) and should never track any tab events or gecko messages michael@0: * on its own to keep it as dumb as possible. michael@0: * michael@0: * The UI has two possible modes: progress and display which are triggered michael@0: * when UpdateFlags.PROGRESS is used depending on the current tab state. michael@0: * The progress mode is triggered when the tab is loading a page. Display mode michael@0: * is used otherwise. michael@0: * michael@0: * {@code ToolbarDisplayLayout} is meant to be owned by {@code BrowserToolbar} michael@0: * which is the main event bus for the toolbar subsystem. michael@0: */ michael@0: public class ToolbarDisplayLayout extends ThemedLinearLayout michael@0: implements Animation.AnimationListener { michael@0: michael@0: private static final String LOGTAG = "GeckoToolbarDisplayLayout"; michael@0: michael@0: // To be used with updateFromTab() to allow the caller michael@0: // to give enough context for the requested state change. michael@0: enum UpdateFlags { michael@0: TITLE, michael@0: FAVICON, michael@0: PROGRESS, michael@0: SITE_IDENTITY, michael@0: PRIVATE_MODE, michael@0: michael@0: // Disable any animation that might be michael@0: // triggered from this state change. Mostly michael@0: // used on tab switches, see BrowserToolbar. michael@0: DISABLE_ANIMATIONS michael@0: } michael@0: michael@0: private enum UIMode { michael@0: PROGRESS, michael@0: DISPLAY michael@0: } michael@0: michael@0: interface OnStopListener { michael@0: public Tab onStop(); michael@0: } michael@0: michael@0: interface OnTitleChangeListener { michael@0: public void onTitleChange(CharSequence title); michael@0: } michael@0: michael@0: private final BrowserApp mActivity; michael@0: michael@0: private UIMode mUiMode; michael@0: michael@0: private ThemedTextView mTitle; michael@0: private int mTitlePadding; michael@0: private ToolbarTitlePrefs mTitlePrefs; michael@0: private OnTitleChangeListener mTitleChangeListener; michael@0: michael@0: private ImageButton mSiteSecurity; michael@0: private boolean mSiteSecurityVisible; michael@0: michael@0: // To de-bounce sets. michael@0: private Bitmap mLastFavicon; michael@0: private ImageButton mFavicon; michael@0: private int mFaviconSize; michael@0: michael@0: private ImageButton mStop; michael@0: private OnStopListener mStopListener; michael@0: michael@0: private PageActionLayout mPageActionLayout; michael@0: michael@0: private AlphaAnimation mLockFadeIn; michael@0: private TranslateAnimation mTitleSlideLeft; michael@0: private TranslateAnimation mTitleSlideRight; michael@0: michael@0: private SiteIdentityPopup mSiteIdentityPopup; michael@0: private SecurityMode mSecurityMode; michael@0: michael@0: private PropertyAnimator mForwardAnim; michael@0: michael@0: private final ForegroundColorSpan mUrlColor; michael@0: private final ForegroundColorSpan mBlockedColor; michael@0: private final ForegroundColorSpan mDomainColor; michael@0: private final ForegroundColorSpan mPrivateDomainColor; michael@0: michael@0: public ToolbarDisplayLayout(Context context, AttributeSet attrs) { michael@0: super(context, attrs); michael@0: setOrientation(HORIZONTAL); michael@0: michael@0: mActivity = (BrowserApp) context; michael@0: michael@0: LayoutInflater.from(context).inflate(R.layout.toolbar_display_layout, this); michael@0: michael@0: mTitle = (ThemedTextView) findViewById(R.id.url_bar_title); michael@0: mTitlePadding = mTitle.getPaddingRight(); michael@0: michael@0: final Resources res = getResources(); michael@0: michael@0: mUrlColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_urltext)); michael@0: mBlockedColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_blockedtext)); michael@0: mDomainColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_domaintext)); michael@0: mPrivateDomainColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_domaintext_private)); michael@0: michael@0: mFavicon = (ImageButton) findViewById(R.id.favicon); michael@0: if (Build.VERSION.SDK_INT >= 16) { michael@0: mFavicon.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); michael@0: } michael@0: mFaviconSize = Math.round(res.getDimension(R.dimen.browser_toolbar_favicon_size)); michael@0: michael@0: mSiteSecurity = (ImageButton) findViewById(R.id.site_security); michael@0: mSiteSecurityVisible = (mSiteSecurity.getVisibility() == View.VISIBLE); michael@0: michael@0: mSiteIdentityPopup = new SiteIdentityPopup(mActivity); michael@0: mSiteIdentityPopup.setAnchor(mSiteSecurity); michael@0: michael@0: mStop = (ImageButton) findViewById(R.id.stop); michael@0: mPageActionLayout = (PageActionLayout) findViewById(R.id.page_action_layout); michael@0: } michael@0: michael@0: @Override michael@0: public void onAttachedToWindow() { michael@0: mTitlePrefs = new ToolbarTitlePrefs(); michael@0: michael@0: Button.OnClickListener faviconListener = new Button.OnClickListener() { michael@0: @Override michael@0: public void onClick(View view) { michael@0: if (mSiteSecurity.getVisibility() != View.VISIBLE) { michael@0: return; michael@0: } michael@0: michael@0: mSiteIdentityPopup.show(); michael@0: } michael@0: }; michael@0: michael@0: mFavicon.setOnClickListener(faviconListener); michael@0: mSiteSecurity.setOnClickListener(faviconListener); michael@0: michael@0: mStop.setOnClickListener(new Button.OnClickListener() { michael@0: @Override michael@0: public void onClick(View v) { michael@0: if (mStopListener != null) { michael@0: // Force toolbar to switch to Display mode michael@0: // immediately based on the stopped tab. michael@0: final Tab tab = mStopListener.onStop(); michael@0: if (tab != null) { michael@0: updateUiMode(tab, UIMode.DISPLAY, EnumSet.noneOf(UpdateFlags.class)); michael@0: } michael@0: } michael@0: } michael@0: }); michael@0: michael@0: float slideWidth = getResources().getDimension(R.dimen.browser_toolbar_lock_width); michael@0: michael@0: LayoutParams siteSecParams = (LayoutParams) mSiteSecurity.getLayoutParams(); michael@0: final float scale = getResources().getDisplayMetrics().density; michael@0: slideWidth += (siteSecParams.leftMargin + siteSecParams.rightMargin) * scale + 0.5f; michael@0: michael@0: mLockFadeIn = new AlphaAnimation(0.0f, 1.0f); michael@0: mLockFadeIn.setAnimationListener(this); michael@0: michael@0: mTitleSlideLeft = new TranslateAnimation(slideWidth, 0, 0, 0); michael@0: mTitleSlideLeft.setAnimationListener(this); michael@0: michael@0: mTitleSlideRight = new TranslateAnimation(-slideWidth, 0, 0, 0); michael@0: mTitleSlideRight.setAnimationListener(this); michael@0: michael@0: final int lockAnimDuration = 300; michael@0: mLockFadeIn.setDuration(lockAnimDuration); michael@0: mTitleSlideLeft.setDuration(lockAnimDuration); michael@0: mTitleSlideRight.setDuration(lockAnimDuration); michael@0: } michael@0: michael@0: @Override michael@0: public void onDetachedFromWindow() { michael@0: mTitlePrefs.close(); michael@0: } michael@0: michael@0: @Override michael@0: public void onAnimationStart(Animation animation) { michael@0: if (animation.equals(mLockFadeIn)) { michael@0: if (mSiteSecurityVisible) michael@0: mSiteSecurity.setVisibility(View.VISIBLE); michael@0: } else if (animation.equals(mTitleSlideLeft)) { michael@0: // These two animations may be scheduled to start while the forward michael@0: // animation is occurring. If we're showing the site security icon, make michael@0: // sure it doesn't take any space during the forward transition. michael@0: mSiteSecurity.setVisibility(View.GONE); michael@0: } else if (animation.equals(mTitleSlideRight)) { michael@0: // If we're hiding the icon, make sure that we keep its padding michael@0: // in place during the forward transition michael@0: mSiteSecurity.setVisibility(View.INVISIBLE); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onAnimationRepeat(Animation animation) { michael@0: } michael@0: michael@0: @Override michael@0: public void onAnimationEnd(Animation animation) { michael@0: if (animation.equals(mTitleSlideRight)) { michael@0: mSiteSecurity.startAnimation(mLockFadeIn); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void setNextFocusDownId(int nextId) { michael@0: mFavicon.setNextFocusDownId(nextId); michael@0: mStop.setNextFocusDownId(nextId); michael@0: mSiteSecurity.setNextFocusDownId(nextId); michael@0: mPageActionLayout.setNextFocusDownId(nextId); michael@0: } michael@0: michael@0: void updateFromTab(Tab tab, EnumSet flags) { michael@0: if (flags.contains(UpdateFlags.TITLE)) { michael@0: updateTitle(tab); michael@0: } michael@0: michael@0: if (flags.contains(UpdateFlags.FAVICON)) { michael@0: updateFavicon(tab); michael@0: } michael@0: michael@0: if (flags.contains(UpdateFlags.SITE_IDENTITY)) { michael@0: updateSiteIdentity(tab, flags); michael@0: } michael@0: michael@0: if (flags.contains(UpdateFlags.PROGRESS)) { michael@0: updateProgress(tab, flags); michael@0: } michael@0: michael@0: if (flags.contains(UpdateFlags.PRIVATE_MODE)) { michael@0: mTitle.setPrivateMode(tab != null && tab.isPrivate()); michael@0: } michael@0: } michael@0: michael@0: void setTitle(CharSequence title) { michael@0: mTitle.setText(title); michael@0: michael@0: if (mTitleChangeListener != null) { michael@0: mTitleChangeListener.onTitleChange(title); michael@0: } michael@0: } michael@0: michael@0: private void updateTitle(Tab tab) { michael@0: // Keep the title unchanged if there's no selected tab, michael@0: // or if the tab is entering reader mode. michael@0: if (tab == null || tab.isEnteringReaderMode()) { michael@0: return; michael@0: } michael@0: michael@0: final String url = tab.getURL(); michael@0: michael@0: // Setting a null title will ensure we just see the michael@0: // "Enter Search or Address" placeholder text. michael@0: if (AboutPages.isTitlelessAboutPage(url)) { michael@0: setTitle(null); michael@0: return; michael@0: } michael@0: michael@0: // Show the about:blocked page title in red, regardless of prefs michael@0: if (tab.getErrorType() == Tab.ErrorType.BLOCKED) { michael@0: final String title = tab.getDisplayTitle(); michael@0: michael@0: final SpannableStringBuilder builder = new SpannableStringBuilder(title); michael@0: builder.setSpan(mBlockedColor, 0, title.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); michael@0: michael@0: setTitle(builder); michael@0: return; michael@0: } michael@0: michael@0: // If the pref to show the URL isn't set, just use the tab's display title. michael@0: if (!mTitlePrefs.shouldShowUrl() || url == null) { michael@0: setTitle(tab.getDisplayTitle()); michael@0: return; michael@0: } michael@0: michael@0: CharSequence title = url; michael@0: if (mTitlePrefs.shouldTrimUrls()) { michael@0: title = StringUtils.stripCommonSubdomains(StringUtils.stripScheme(url)); michael@0: } michael@0: michael@0: final String baseDomain = tab.getBaseDomain(); michael@0: if (!TextUtils.isEmpty(baseDomain)) { michael@0: final SpannableStringBuilder builder = new SpannableStringBuilder(title); michael@0: michael@0: int index = title.toString().indexOf(baseDomain); michael@0: if (index > -1) { michael@0: builder.setSpan(mUrlColor, 0, title.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); michael@0: builder.setSpan(tab.isPrivate() ? mPrivateDomainColor : mDomainColor, michael@0: index, index + baseDomain.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); michael@0: michael@0: title = builder; michael@0: } michael@0: } michael@0: michael@0: setTitle(title); michael@0: } michael@0: michael@0: private void updateFavicon(Tab tab) { michael@0: if (tab == null) { michael@0: mFavicon.setImageDrawable(null); michael@0: return; michael@0: } michael@0: michael@0: Bitmap image = tab.getFavicon(); michael@0: michael@0: if (image != null && image == mLastFavicon) { michael@0: Log.d(LOGTAG, "Ignoring favicon: new image is identical to previous one."); michael@0: return; michael@0: } michael@0: michael@0: // Cache the original so we can debounce without scaling michael@0: mLastFavicon = image; michael@0: michael@0: Log.d(LOGTAG, "updateFavicon(" + image + ")"); michael@0: michael@0: if (image != null) { michael@0: image = Bitmap.createScaledBitmap(image, mFaviconSize, mFaviconSize, false); michael@0: mFavicon.setImageBitmap(image); michael@0: } else { michael@0: mFavicon.setImageResource(R.drawable.favicon); michael@0: } michael@0: } michael@0: michael@0: private void updateSiteIdentity(Tab tab, EnumSet flags) { michael@0: final SiteIdentity siteIdentity; michael@0: if (tab == null) { michael@0: siteIdentity = null; michael@0: } else { michael@0: siteIdentity = tab.getSiteIdentity(); michael@0: } michael@0: michael@0: mSiteIdentityPopup.setSiteIdentity(siteIdentity); michael@0: michael@0: final SecurityMode securityMode; michael@0: if (siteIdentity == null) { michael@0: securityMode = SecurityMode.UNKNOWN; michael@0: } else { michael@0: securityMode = siteIdentity.getSecurityMode(); michael@0: } michael@0: michael@0: if (mSecurityMode != securityMode) { michael@0: mSecurityMode = securityMode; michael@0: mSiteSecurity.setImageLevel(mSecurityMode.ordinal()); michael@0: updatePageActions(flags); michael@0: } michael@0: } michael@0: michael@0: private void updateProgress(Tab tab, EnumSet flags) { michael@0: final boolean shouldShowThrobber = (tab != null && michael@0: tab.getState() == Tab.STATE_LOADING); michael@0: michael@0: updateUiMode(tab, shouldShowThrobber ? UIMode.PROGRESS : UIMode.DISPLAY, flags); michael@0: } michael@0: michael@0: private void updateUiMode(Tab tab, UIMode uiMode, EnumSet flags) { michael@0: if (mUiMode == uiMode) { michael@0: return; michael@0: } michael@0: michael@0: mUiMode = uiMode; michael@0: michael@0: // The "Throbber start" and "Throbber stop" log messages in this method michael@0: // are needed by S1/S2 tests (http://mrcote.info/phonedash/#). michael@0: // See discussion in Bug 804457. Bug 805124 tracks paring these down. michael@0: if (mUiMode == UIMode.PROGRESS) { michael@0: Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber start"); michael@0: } else { michael@0: Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber stop"); michael@0: } michael@0: michael@0: updatePageActions(flags); michael@0: } michael@0: michael@0: private void updatePageActions(EnumSet flags) { michael@0: final boolean isShowingProgress = (mUiMode == UIMode.PROGRESS); michael@0: michael@0: mStop.setVisibility(isShowingProgress ? View.VISIBLE : View.GONE); michael@0: mPageActionLayout.setVisibility(!isShowingProgress ? View.VISIBLE : View.GONE); michael@0: michael@0: boolean shouldShowSiteSecurity = (!isShowingProgress && michael@0: mSecurityMode != SecurityMode.UNKNOWN); michael@0: michael@0: setSiteSecurityVisibility(shouldShowSiteSecurity, flags); michael@0: michael@0: // We want title to fill the whole space available for it when there are icons michael@0: // being shown on the right side of the toolbar as the icons already have some michael@0: // padding in them. This is just to avoid wasting space when icons are shown. michael@0: mTitle.setPadding(0, 0, (!isShowingProgress ? mTitlePadding : 0), 0); michael@0: } michael@0: michael@0: private void setSiteSecurityVisibility(boolean visible, EnumSet flags) { michael@0: if (visible == mSiteSecurityVisible) { michael@0: return; michael@0: } michael@0: michael@0: mSiteSecurityVisible = visible; michael@0: michael@0: mTitle.clearAnimation(); michael@0: mSiteSecurity.clearAnimation(); michael@0: michael@0: if (flags.contains(UpdateFlags.DISABLE_ANIMATIONS)) { michael@0: mSiteSecurity.setVisibility(visible ? View.VISIBLE : View.GONE); michael@0: return; michael@0: } michael@0: michael@0: // If any of these animations were cancelled as a result of the michael@0: // clearAnimation() calls above, we need to reset them. michael@0: mLockFadeIn.reset(); michael@0: mTitleSlideLeft.reset(); michael@0: mTitleSlideRight.reset(); michael@0: michael@0: if (mForwardAnim != null) { michael@0: long delay = mForwardAnim.getRemainingTime(); michael@0: mTitleSlideRight.setStartOffset(delay); michael@0: mTitleSlideLeft.setStartOffset(delay); michael@0: } else { michael@0: mTitleSlideRight.setStartOffset(0); michael@0: mTitleSlideLeft.setStartOffset(0); michael@0: } michael@0: michael@0: mTitle.startAnimation(visible ? mTitleSlideRight : mTitleSlideLeft); michael@0: } michael@0: michael@0: List getFocusOrder() { michael@0: return Arrays.asList(mSiteSecurity, mPageActionLayout, mStop); michael@0: } michael@0: michael@0: void setOnStopListener(OnStopListener listener) { michael@0: mStopListener = listener; michael@0: } michael@0: michael@0: void setOnTitleChangeListener(OnTitleChangeListener listener) { michael@0: mTitleChangeListener = listener; michael@0: } michael@0: michael@0: View getDoorHangerAnchor() { michael@0: return mFavicon; michael@0: } michael@0: michael@0: void prepareForwardAnimation(PropertyAnimator anim, ForwardButtonAnimation animation, int width) { michael@0: mForwardAnim = anim; michael@0: michael@0: if (animation == ForwardButtonAnimation.HIDE) { michael@0: anim.attach(mTitle, michael@0: PropertyAnimator.Property.TRANSLATION_X, michael@0: 0); michael@0: anim.attach(mFavicon, michael@0: PropertyAnimator.Property.TRANSLATION_X, michael@0: 0); michael@0: anim.attach(mSiteSecurity, michael@0: PropertyAnimator.Property.TRANSLATION_X, michael@0: 0); michael@0: michael@0: // We're hiding the forward button. We're going to reset the margin before michael@0: // the animation starts, so we shift these items to the right so that they don't michael@0: // appear to move initially. michael@0: ViewHelper.setTranslationX(mTitle, width); michael@0: ViewHelper.setTranslationX(mFavicon, width); michael@0: ViewHelper.setTranslationX(mSiteSecurity, width); michael@0: } else { michael@0: anim.attach(mTitle, michael@0: PropertyAnimator.Property.TRANSLATION_X, michael@0: width); michael@0: anim.attach(mFavicon, michael@0: PropertyAnimator.Property.TRANSLATION_X, michael@0: width); michael@0: anim.attach(mSiteSecurity, michael@0: PropertyAnimator.Property.TRANSLATION_X, michael@0: width); michael@0: } michael@0: } michael@0: michael@0: void finishForwardAnimation() { michael@0: ViewHelper.setTranslationX(mTitle, 0); michael@0: ViewHelper.setTranslationX(mFavicon, 0); michael@0: ViewHelper.setTranslationX(mSiteSecurity, 0); michael@0: michael@0: mForwardAnim = null; michael@0: } michael@0: michael@0: void prepareStartEditingAnimation() { michael@0: // Hide page actions/stop buttons immediately michael@0: ViewHelper.setAlpha(mPageActionLayout, 0); michael@0: ViewHelper.setAlpha(mStop, 0); michael@0: } michael@0: michael@0: void prepareStopEditingAnimation(PropertyAnimator anim) { michael@0: // Fade toolbar buttons (page actions, stop) after the entry michael@0: // is schrunk back to its original size. michael@0: anim.attach(mPageActionLayout, michael@0: PropertyAnimator.Property.ALPHA, michael@0: 1); michael@0: michael@0: anim.attach(mStop, michael@0: PropertyAnimator.Property.ALPHA, michael@0: 1); michael@0: } michael@0: michael@0: boolean dismissSiteIdentityPopup() { michael@0: if (mSiteIdentityPopup != null && mSiteIdentityPopup.isShowing()) { michael@0: mSiteIdentityPopup.dismiss(); michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: }