Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 package org.mozilla.gecko.toolbar;
8 import org.mozilla.gecko.AboutPages;
9 import org.mozilla.gecko.animation.PropertyAnimator;
10 import org.mozilla.gecko.animation.ViewHelper;
11 import org.mozilla.gecko.BrowserApp;
12 import org.mozilla.gecko.R;
13 import org.mozilla.gecko.SiteIdentity;
14 import org.mozilla.gecko.SiteIdentity.SecurityMode;
15 import org.mozilla.gecko.Tab;
16 import org.mozilla.gecko.Tabs;
17 import org.mozilla.gecko.toolbar.BrowserToolbar.ForwardButtonAnimation;
18 import org.mozilla.gecko.util.StringUtils;
19 import org.mozilla.gecko.widget.ThemedLinearLayout;
20 import org.mozilla.gecko.widget.ThemedTextView;
22 import org.json.JSONObject;
24 import android.content.Context;
25 import android.content.res.Resources;
26 import android.graphics.Bitmap;
27 import android.os.Build;
28 import android.os.SystemClock;
29 import android.text.style.ForegroundColorSpan;
30 import android.text.Spannable;
31 import android.text.SpannableStringBuilder;
32 import android.text.Spanned;
33 import android.text.TextUtils;
34 import android.util.AttributeSet;
35 import android.util.Log;
36 import android.view.LayoutInflater;
37 import android.view.View;
38 import android.view.animation.Animation;
39 import android.view.animation.AnimationUtils;
40 import android.view.animation.AlphaAnimation;
41 import android.view.animation.TranslateAnimation;
42 import android.widget.Button;
43 import android.widget.ImageButton;
44 import android.widget.LinearLayout.LayoutParams;
46 import java.util.Arrays;
47 import java.util.EnumSet;
48 import java.util.List;
50 /**
51 * {@code ToolbarDisplayLayout} is the UI for when the toolbar is in
52 * display state. It's used to display the state of the currently selected
53 * tab. It should always be updated through a single entry point
54 * (updateFromTab) and should never track any tab events or gecko messages
55 * on its own to keep it as dumb as possible.
56 *
57 * The UI has two possible modes: progress and display which are triggered
58 * when UpdateFlags.PROGRESS is used depending on the current tab state.
59 * The progress mode is triggered when the tab is loading a page. Display mode
60 * is used otherwise.
61 *
62 * {@code ToolbarDisplayLayout} is meant to be owned by {@code BrowserToolbar}
63 * which is the main event bus for the toolbar subsystem.
64 */
65 public class ToolbarDisplayLayout extends ThemedLinearLayout
66 implements Animation.AnimationListener {
68 private static final String LOGTAG = "GeckoToolbarDisplayLayout";
70 // To be used with updateFromTab() to allow the caller
71 // to give enough context for the requested state change.
72 enum UpdateFlags {
73 TITLE,
74 FAVICON,
75 PROGRESS,
76 SITE_IDENTITY,
77 PRIVATE_MODE,
79 // Disable any animation that might be
80 // triggered from this state change. Mostly
81 // used on tab switches, see BrowserToolbar.
82 DISABLE_ANIMATIONS
83 }
85 private enum UIMode {
86 PROGRESS,
87 DISPLAY
88 }
90 interface OnStopListener {
91 public Tab onStop();
92 }
94 interface OnTitleChangeListener {
95 public void onTitleChange(CharSequence title);
96 }
98 private final BrowserApp mActivity;
100 private UIMode mUiMode;
102 private ThemedTextView mTitle;
103 private int mTitlePadding;
104 private ToolbarTitlePrefs mTitlePrefs;
105 private OnTitleChangeListener mTitleChangeListener;
107 private ImageButton mSiteSecurity;
108 private boolean mSiteSecurityVisible;
110 // To de-bounce sets.
111 private Bitmap mLastFavicon;
112 private ImageButton mFavicon;
113 private int mFaviconSize;
115 private ImageButton mStop;
116 private OnStopListener mStopListener;
118 private PageActionLayout mPageActionLayout;
120 private AlphaAnimation mLockFadeIn;
121 private TranslateAnimation mTitleSlideLeft;
122 private TranslateAnimation mTitleSlideRight;
124 private SiteIdentityPopup mSiteIdentityPopup;
125 private SecurityMode mSecurityMode;
127 private PropertyAnimator mForwardAnim;
129 private final ForegroundColorSpan mUrlColor;
130 private final ForegroundColorSpan mBlockedColor;
131 private final ForegroundColorSpan mDomainColor;
132 private final ForegroundColorSpan mPrivateDomainColor;
134 public ToolbarDisplayLayout(Context context, AttributeSet attrs) {
135 super(context, attrs);
136 setOrientation(HORIZONTAL);
138 mActivity = (BrowserApp) context;
140 LayoutInflater.from(context).inflate(R.layout.toolbar_display_layout, this);
142 mTitle = (ThemedTextView) findViewById(R.id.url_bar_title);
143 mTitlePadding = mTitle.getPaddingRight();
145 final Resources res = getResources();
147 mUrlColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_urltext));
148 mBlockedColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_blockedtext));
149 mDomainColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_domaintext));
150 mPrivateDomainColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_domaintext_private));
152 mFavicon = (ImageButton) findViewById(R.id.favicon);
153 if (Build.VERSION.SDK_INT >= 16) {
154 mFavicon.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
155 }
156 mFaviconSize = Math.round(res.getDimension(R.dimen.browser_toolbar_favicon_size));
158 mSiteSecurity = (ImageButton) findViewById(R.id.site_security);
159 mSiteSecurityVisible = (mSiteSecurity.getVisibility() == View.VISIBLE);
161 mSiteIdentityPopup = new SiteIdentityPopup(mActivity);
162 mSiteIdentityPopup.setAnchor(mSiteSecurity);
164 mStop = (ImageButton) findViewById(R.id.stop);
165 mPageActionLayout = (PageActionLayout) findViewById(R.id.page_action_layout);
166 }
168 @Override
169 public void onAttachedToWindow() {
170 mTitlePrefs = new ToolbarTitlePrefs();
172 Button.OnClickListener faviconListener = new Button.OnClickListener() {
173 @Override
174 public void onClick(View view) {
175 if (mSiteSecurity.getVisibility() != View.VISIBLE) {
176 return;
177 }
179 mSiteIdentityPopup.show();
180 }
181 };
183 mFavicon.setOnClickListener(faviconListener);
184 mSiteSecurity.setOnClickListener(faviconListener);
186 mStop.setOnClickListener(new Button.OnClickListener() {
187 @Override
188 public void onClick(View v) {
189 if (mStopListener != null) {
190 // Force toolbar to switch to Display mode
191 // immediately based on the stopped tab.
192 final Tab tab = mStopListener.onStop();
193 if (tab != null) {
194 updateUiMode(tab, UIMode.DISPLAY, EnumSet.noneOf(UpdateFlags.class));
195 }
196 }
197 }
198 });
200 float slideWidth = getResources().getDimension(R.dimen.browser_toolbar_lock_width);
202 LayoutParams siteSecParams = (LayoutParams) mSiteSecurity.getLayoutParams();
203 final float scale = getResources().getDisplayMetrics().density;
204 slideWidth += (siteSecParams.leftMargin + siteSecParams.rightMargin) * scale + 0.5f;
206 mLockFadeIn = new AlphaAnimation(0.0f, 1.0f);
207 mLockFadeIn.setAnimationListener(this);
209 mTitleSlideLeft = new TranslateAnimation(slideWidth, 0, 0, 0);
210 mTitleSlideLeft.setAnimationListener(this);
212 mTitleSlideRight = new TranslateAnimation(-slideWidth, 0, 0, 0);
213 mTitleSlideRight.setAnimationListener(this);
215 final int lockAnimDuration = 300;
216 mLockFadeIn.setDuration(lockAnimDuration);
217 mTitleSlideLeft.setDuration(lockAnimDuration);
218 mTitleSlideRight.setDuration(lockAnimDuration);
219 }
221 @Override
222 public void onDetachedFromWindow() {
223 mTitlePrefs.close();
224 }
226 @Override
227 public void onAnimationStart(Animation animation) {
228 if (animation.equals(mLockFadeIn)) {
229 if (mSiteSecurityVisible)
230 mSiteSecurity.setVisibility(View.VISIBLE);
231 } else if (animation.equals(mTitleSlideLeft)) {
232 // These two animations may be scheduled to start while the forward
233 // animation is occurring. If we're showing the site security icon, make
234 // sure it doesn't take any space during the forward transition.
235 mSiteSecurity.setVisibility(View.GONE);
236 } else if (animation.equals(mTitleSlideRight)) {
237 // If we're hiding the icon, make sure that we keep its padding
238 // in place during the forward transition
239 mSiteSecurity.setVisibility(View.INVISIBLE);
240 }
241 }
243 @Override
244 public void onAnimationRepeat(Animation animation) {
245 }
247 @Override
248 public void onAnimationEnd(Animation animation) {
249 if (animation.equals(mTitleSlideRight)) {
250 mSiteSecurity.startAnimation(mLockFadeIn);
251 }
252 }
254 @Override
255 public void setNextFocusDownId(int nextId) {
256 mFavicon.setNextFocusDownId(nextId);
257 mStop.setNextFocusDownId(nextId);
258 mSiteSecurity.setNextFocusDownId(nextId);
259 mPageActionLayout.setNextFocusDownId(nextId);
260 }
262 void updateFromTab(Tab tab, EnumSet<UpdateFlags> flags) {
263 if (flags.contains(UpdateFlags.TITLE)) {
264 updateTitle(tab);
265 }
267 if (flags.contains(UpdateFlags.FAVICON)) {
268 updateFavicon(tab);
269 }
271 if (flags.contains(UpdateFlags.SITE_IDENTITY)) {
272 updateSiteIdentity(tab, flags);
273 }
275 if (flags.contains(UpdateFlags.PROGRESS)) {
276 updateProgress(tab, flags);
277 }
279 if (flags.contains(UpdateFlags.PRIVATE_MODE)) {
280 mTitle.setPrivateMode(tab != null && tab.isPrivate());
281 }
282 }
284 void setTitle(CharSequence title) {
285 mTitle.setText(title);
287 if (mTitleChangeListener != null) {
288 mTitleChangeListener.onTitleChange(title);
289 }
290 }
292 private void updateTitle(Tab tab) {
293 // Keep the title unchanged if there's no selected tab,
294 // or if the tab is entering reader mode.
295 if (tab == null || tab.isEnteringReaderMode()) {
296 return;
297 }
299 final String url = tab.getURL();
301 // Setting a null title will ensure we just see the
302 // "Enter Search or Address" placeholder text.
303 if (AboutPages.isTitlelessAboutPage(url)) {
304 setTitle(null);
305 return;
306 }
308 // Show the about:blocked page title in red, regardless of prefs
309 if (tab.getErrorType() == Tab.ErrorType.BLOCKED) {
310 final String title = tab.getDisplayTitle();
312 final SpannableStringBuilder builder = new SpannableStringBuilder(title);
313 builder.setSpan(mBlockedColor, 0, title.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
315 setTitle(builder);
316 return;
317 }
319 // If the pref to show the URL isn't set, just use the tab's display title.
320 if (!mTitlePrefs.shouldShowUrl() || url == null) {
321 setTitle(tab.getDisplayTitle());
322 return;
323 }
325 CharSequence title = url;
326 if (mTitlePrefs.shouldTrimUrls()) {
327 title = StringUtils.stripCommonSubdomains(StringUtils.stripScheme(url));
328 }
330 final String baseDomain = tab.getBaseDomain();
331 if (!TextUtils.isEmpty(baseDomain)) {
332 final SpannableStringBuilder builder = new SpannableStringBuilder(title);
334 int index = title.toString().indexOf(baseDomain);
335 if (index > -1) {
336 builder.setSpan(mUrlColor, 0, title.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
337 builder.setSpan(tab.isPrivate() ? mPrivateDomainColor : mDomainColor,
338 index, index + baseDomain.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
340 title = builder;
341 }
342 }
344 setTitle(title);
345 }
347 private void updateFavicon(Tab tab) {
348 if (tab == null) {
349 mFavicon.setImageDrawable(null);
350 return;
351 }
353 Bitmap image = tab.getFavicon();
355 if (image != null && image == mLastFavicon) {
356 Log.d(LOGTAG, "Ignoring favicon: new image is identical to previous one.");
357 return;
358 }
360 // Cache the original so we can debounce without scaling
361 mLastFavicon = image;
363 Log.d(LOGTAG, "updateFavicon(" + image + ")");
365 if (image != null) {
366 image = Bitmap.createScaledBitmap(image, mFaviconSize, mFaviconSize, false);
367 mFavicon.setImageBitmap(image);
368 } else {
369 mFavicon.setImageResource(R.drawable.favicon);
370 }
371 }
373 private void updateSiteIdentity(Tab tab, EnumSet<UpdateFlags> flags) {
374 final SiteIdentity siteIdentity;
375 if (tab == null) {
376 siteIdentity = null;
377 } else {
378 siteIdentity = tab.getSiteIdentity();
379 }
381 mSiteIdentityPopup.setSiteIdentity(siteIdentity);
383 final SecurityMode securityMode;
384 if (siteIdentity == null) {
385 securityMode = SecurityMode.UNKNOWN;
386 } else {
387 securityMode = siteIdentity.getSecurityMode();
388 }
390 if (mSecurityMode != securityMode) {
391 mSecurityMode = securityMode;
392 mSiteSecurity.setImageLevel(mSecurityMode.ordinal());
393 updatePageActions(flags);
394 }
395 }
397 private void updateProgress(Tab tab, EnumSet<UpdateFlags> flags) {
398 final boolean shouldShowThrobber = (tab != null &&
399 tab.getState() == Tab.STATE_LOADING);
401 updateUiMode(tab, shouldShowThrobber ? UIMode.PROGRESS : UIMode.DISPLAY, flags);
402 }
404 private void updateUiMode(Tab tab, UIMode uiMode, EnumSet<UpdateFlags> flags) {
405 if (mUiMode == uiMode) {
406 return;
407 }
409 mUiMode = uiMode;
411 // The "Throbber start" and "Throbber stop" log messages in this method
412 // are needed by S1/S2 tests (http://mrcote.info/phonedash/#).
413 // See discussion in Bug 804457. Bug 805124 tracks paring these down.
414 if (mUiMode == UIMode.PROGRESS) {
415 Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber start");
416 } else {
417 Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber stop");
418 }
420 updatePageActions(flags);
421 }
423 private void updatePageActions(EnumSet<UpdateFlags> flags) {
424 final boolean isShowingProgress = (mUiMode == UIMode.PROGRESS);
426 mStop.setVisibility(isShowingProgress ? View.VISIBLE : View.GONE);
427 mPageActionLayout.setVisibility(!isShowingProgress ? View.VISIBLE : View.GONE);
429 boolean shouldShowSiteSecurity = (!isShowingProgress &&
430 mSecurityMode != SecurityMode.UNKNOWN);
432 setSiteSecurityVisibility(shouldShowSiteSecurity, flags);
434 // We want title to fill the whole space available for it when there are icons
435 // being shown on the right side of the toolbar as the icons already have some
436 // padding in them. This is just to avoid wasting space when icons are shown.
437 mTitle.setPadding(0, 0, (!isShowingProgress ? mTitlePadding : 0), 0);
438 }
440 private void setSiteSecurityVisibility(boolean visible, EnumSet<UpdateFlags> flags) {
441 if (visible == mSiteSecurityVisible) {
442 return;
443 }
445 mSiteSecurityVisible = visible;
447 mTitle.clearAnimation();
448 mSiteSecurity.clearAnimation();
450 if (flags.contains(UpdateFlags.DISABLE_ANIMATIONS)) {
451 mSiteSecurity.setVisibility(visible ? View.VISIBLE : View.GONE);
452 return;
453 }
455 // If any of these animations were cancelled as a result of the
456 // clearAnimation() calls above, we need to reset them.
457 mLockFadeIn.reset();
458 mTitleSlideLeft.reset();
459 mTitleSlideRight.reset();
461 if (mForwardAnim != null) {
462 long delay = mForwardAnim.getRemainingTime();
463 mTitleSlideRight.setStartOffset(delay);
464 mTitleSlideLeft.setStartOffset(delay);
465 } else {
466 mTitleSlideRight.setStartOffset(0);
467 mTitleSlideLeft.setStartOffset(0);
468 }
470 mTitle.startAnimation(visible ? mTitleSlideRight : mTitleSlideLeft);
471 }
473 List<View> getFocusOrder() {
474 return Arrays.asList(mSiteSecurity, mPageActionLayout, mStop);
475 }
477 void setOnStopListener(OnStopListener listener) {
478 mStopListener = listener;
479 }
481 void setOnTitleChangeListener(OnTitleChangeListener listener) {
482 mTitleChangeListener = listener;
483 }
485 View getDoorHangerAnchor() {
486 return mFavicon;
487 }
489 void prepareForwardAnimation(PropertyAnimator anim, ForwardButtonAnimation animation, int width) {
490 mForwardAnim = anim;
492 if (animation == ForwardButtonAnimation.HIDE) {
493 anim.attach(mTitle,
494 PropertyAnimator.Property.TRANSLATION_X,
495 0);
496 anim.attach(mFavicon,
497 PropertyAnimator.Property.TRANSLATION_X,
498 0);
499 anim.attach(mSiteSecurity,
500 PropertyAnimator.Property.TRANSLATION_X,
501 0);
503 // We're hiding the forward button. We're going to reset the margin before
504 // the animation starts, so we shift these items to the right so that they don't
505 // appear to move initially.
506 ViewHelper.setTranslationX(mTitle, width);
507 ViewHelper.setTranslationX(mFavicon, width);
508 ViewHelper.setTranslationX(mSiteSecurity, width);
509 } else {
510 anim.attach(mTitle,
511 PropertyAnimator.Property.TRANSLATION_X,
512 width);
513 anim.attach(mFavicon,
514 PropertyAnimator.Property.TRANSLATION_X,
515 width);
516 anim.attach(mSiteSecurity,
517 PropertyAnimator.Property.TRANSLATION_X,
518 width);
519 }
520 }
522 void finishForwardAnimation() {
523 ViewHelper.setTranslationX(mTitle, 0);
524 ViewHelper.setTranslationX(mFavicon, 0);
525 ViewHelper.setTranslationX(mSiteSecurity, 0);
527 mForwardAnim = null;
528 }
530 void prepareStartEditingAnimation() {
531 // Hide page actions/stop buttons immediately
532 ViewHelper.setAlpha(mPageActionLayout, 0);
533 ViewHelper.setAlpha(mStop, 0);
534 }
536 void prepareStopEditingAnimation(PropertyAnimator anim) {
537 // Fade toolbar buttons (page actions, stop) after the entry
538 // is schrunk back to its original size.
539 anim.attach(mPageActionLayout,
540 PropertyAnimator.Property.ALPHA,
541 1);
543 anim.attach(mStop,
544 PropertyAnimator.Property.ALPHA,
545 1);
546 }
548 boolean dismissSiteIdentityPopup() {
549 if (mSiteIdentityPopup != null && mSiteIdentityPopup.isShowing()) {
550 mSiteIdentityPopup.dismiss();
551 return true;
552 }
554 return false;
555 }
556 }