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/. */ 5 6 package org.mozilla.gecko.toolbar; 7 8 import java.util.ArrayList; 9 import java.util.EnumSet; 10 import java.util.List; 11 12 import android.graphics.Rect; 13 import android.support.annotation.Nullable; 14 import android.support.v4.content.ContextCompat; 15 import org.mozilla.gecko.BrowserApp; 16 import org.mozilla.gecko.Clipboard; 17 import org.mozilla.gecko.GeckoSharedPrefs; 18 import org.mozilla.gecko.R; 19 import org.mozilla.gecko.Tab; 20 import org.mozilla.gecko.Tabs; 21 import org.mozilla.gecko.Telemetry; 22 import org.mozilla.gecko.TelemetryContract; 23 import org.mozilla.gecko.TouchEventInterceptor; 24 import org.mozilla.gecko.animation.PropertyAnimator; 25 import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener; 26 import org.mozilla.gecko.animation.ViewHelper; 27 import org.mozilla.gecko.lwt.LightweightTheme; 28 import org.mozilla.gecko.lwt.LightweightThemeDrawable; 29 import org.mozilla.gecko.menu.GeckoMenu; 30 import org.mozilla.gecko.menu.MenuPopup; 31 import org.mozilla.gecko.preferences.GeckoPreferences; 32 import org.mozilla.gecko.tabs.TabHistoryController; 33 import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.OnStopListener; 34 import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.OnTitleChangeListener; 35 import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.UpdateFlags; 36 import org.mozilla.gecko.util.HardwareUtils; 37 import org.mozilla.gecko.util.MenuUtils; 38 import org.mozilla.gecko.util.WindowUtil; 39 import org.mozilla.gecko.widget.AnimatedProgressBar; 40 import org.mozilla.gecko.widget.TouchDelegateWithReset; 41 import org.mozilla.gecko.widget.themed.ThemedImageButton; 42 import org.mozilla.gecko.widget.themed.ThemedRelativeLayout; 43 44 import android.content.Context; 45 import android.content.res.Resources; 46 import android.graphics.Canvas; 47 import android.graphics.Paint; 48 import android.graphics.drawable.Drawable; 49 import android.graphics.drawable.StateListDrawable; 50 import android.text.TextUtils; 51 import android.util.AttributeSet; 52 import android.util.Log; 53 import android.view.ContextMenu; 54 import android.view.LayoutInflater; 55 import android.view.MenuInflater; 56 import android.view.MotionEvent; 57 import android.view.View; 58 import android.view.ViewTreeObserver.OnGlobalLayoutListener; 59 import android.view.inputmethod.InputMethodManager; 60 import android.widget.Button; 61 import android.widget.HorizontalScrollView; 62 import android.widget.LinearLayout; 63 import android.widget.PopupWindow; 64 import android.support.annotation.NonNull; 65 66 /** 67 * {@code BrowserToolbar} is single entry point for users of the toolbar 68 * subsystem i.e. this should be the only import outside the 'toolbar' 69 * package. 70 * 71 * {@code BrowserToolbar} serves at the single event bus for all 72 * sub-components in the toolbar. It tracks tab events and gecko messages 73 * and update the state of its inner components accordingly. 74 * 75 * It has two states, display and edit, which are controlled by 76 * ToolbarEditLayout and ToolbarDisplayLayout. In display state, the toolbar 77 * displays the current state for the selected tab. In edit state, it shows 78 * a text entry for searching bookmarks/history. {@code BrowserToolbar} 79 * provides public API to enter, cancel, and commit the edit state as well 80 * as a set of listeners to allow {@code BrowserToolbar} users to react 81 * to state changes accordingly. 82 */ 83 public abstract class BrowserToolbar extends ThemedRelativeLayout 84 implements Tabs.OnTabsChangedListener, 85 GeckoMenu.ActionItemBarPresenter { 86 private static final String LOGTAG = "GeckoToolbar"; 87 88 private static final int LIGHTWEIGHT_THEME_INVERT_ALPHA_START = 204; // 255 - alpha = invert_alpha 89 private static final int LIGHTWEIGHT_THEME_INVERT_ALPHA_END = 179; 90 public static final int LIGHTWEIGHT_THEME_INVERT_ALPHA_TABLET = 51; 91 92 public interface OnActivateListener { onActivate()93 public void onActivate(); 94 } 95 96 public interface OnCommitListener { onCommit()97 public void onCommit(); 98 } 99 100 public interface OnDismissListener { onDismiss()101 public void onDismiss(); 102 } 103 104 public interface OnFilterListener { onFilter(String searchText, AutocompleteHandler handler)105 public void onFilter(String searchText, AutocompleteHandler handler); 106 } 107 108 public interface OnStartEditingListener { onStartEditing()109 public void onStartEditing(); 110 } 111 112 public interface OnStopEditingListener { onStopEditing()113 public void onStopEditing(); 114 } 115 116 protected enum UIMode { 117 EDIT, 118 DISPLAY 119 } 120 121 protected final ToolbarDisplayLayout urlDisplayLayout; 122 protected final HorizontalScrollView urlDisplayScroll; 123 protected final ToolbarEditLayout urlEditLayout; 124 protected final View urlBarEntry; 125 protected boolean isSwitchingTabs; 126 protected final ThemedImageButton tabsButton; 127 128 private AnimatedProgressBar progressBar; 129 protected final TabCounter tabsCounter; 130 protected final View menuButton; 131 private MenuPopup menuPopup; 132 protected final List<View> focusOrder; 133 134 private OnActivateListener activateListener; 135 private OnFocusChangeListener focusChangeListener; 136 private OnStartEditingListener startEditingListener; 137 private OnStopEditingListener stopEditingListener; 138 private TouchEventInterceptor mTouchEventInterceptor; 139 140 protected final BrowserApp activity; 141 142 protected UIMode uiMode; 143 protected TabHistoryController tabHistoryController; 144 145 private final Paint shadowPaint; 146 private final int shadowColor; 147 private final int shadowPrivateColor; 148 private final int shadowSize; 149 150 private final ToolbarPrefs prefs; 151 isAnimating()152 public abstract boolean isAnimating(); 153 isTabsButtonOffscreen()154 protected abstract boolean isTabsButtonOffscreen(); 155 updateNavigationButtons(Tab tab)156 protected abstract void updateNavigationButtons(Tab tab); 157 triggerStartEditingTransition(PropertyAnimator animator)158 protected abstract void triggerStartEditingTransition(PropertyAnimator animator); triggerStopEditingTransition()159 protected abstract void triggerStopEditingTransition(); triggerTabsPanelTransition(PropertyAnimator animator, boolean areTabsShown)160 public abstract void triggerTabsPanelTransition(PropertyAnimator animator, boolean areTabsShown); 161 162 /** 163 * Returns a Drawable overlaid with the theme's bitmap. 164 */ getLWTDefaultStateSetDrawable()165 protected Drawable getLWTDefaultStateSetDrawable() { 166 return getTheme().getDrawable(this); 167 } 168 create(final Context context, final AttributeSet attrs)169 public static BrowserToolbar create(final Context context, final AttributeSet attrs) { 170 final boolean isLargeResource = context.getResources().getBoolean(R.bool.is_large_resource); 171 final BrowserToolbar toolbar; 172 if (isLargeResource) { 173 toolbar = new BrowserToolbarTablet(context, attrs); 174 } else { 175 toolbar = new BrowserToolbarPhone(context, attrs); 176 } 177 return toolbar; 178 } 179 BrowserToolbar(final Context context, final AttributeSet attrs)180 protected BrowserToolbar(final Context context, final AttributeSet attrs) { 181 super(context, attrs); 182 setWillNotDraw(false); 183 184 // BrowserToolbar is attached to BrowserApp only. 185 activity = (BrowserApp) context; 186 187 LayoutInflater.from(context).inflate(R.layout.browser_toolbar, this); 188 189 Tabs.registerOnTabsChangedListener(this); 190 isSwitchingTabs = true; 191 192 urlDisplayLayout = (ToolbarDisplayLayout) findViewById(R.id.display_layout); 193 urlDisplayScroll = (HorizontalScrollView) findViewById(R.id.url_bar_title_scroll_view); 194 urlBarEntry = findViewById(R.id.url_bar_entry); 195 urlEditLayout = (ToolbarEditLayout) findViewById(R.id.edit_layout); 196 197 tabsButton = (ThemedImageButton) findViewById(R.id.tabs); 198 tabsCounter = (TabCounter) findViewById(R.id.tabs_counter); 199 tabsCounter.setLayerType(View.LAYER_TYPE_SOFTWARE, null); 200 201 menuButton = findViewById(R.id.menu); 202 203 // The focusOrder List should be filled by sub-classes. 204 focusOrder = new ArrayList<View>(); 205 206 final Resources res = getResources(); 207 shadowSize = res.getDimensionPixelSize(R.dimen.browser_toolbar_shadow_size); 208 209 shadowPaint = new Paint(); 210 shadowColor = ContextCompat.getColor(context, R.color.url_bar_shadow); 211 shadowPrivateColor = ContextCompat.getColor(context, R.color.url_bar_shadow_private); 212 shadowPaint.setColor(shadowColor); 213 shadowPaint.setStrokeWidth(0.0f); 214 215 setUIMode(UIMode.DISPLAY); 216 217 prefs = new ToolbarPrefs(); 218 urlDisplayLayout.setToolbarPrefs(prefs); 219 urlEditLayout.setToolbarPrefs(prefs); 220 221 // ScrollViews are allowed to have only one child. 222 final View scrollChild = urlDisplayScroll.getChildAt(0); 223 224 urlDisplayScroll.addOnLayoutChangeListener(new OnLayoutChangeListener() { 225 @Override 226 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { 227 final int width = urlDisplayScroll.getWidth(); 228 final int height = urlDisplayScroll.getHeight(); 229 final int oldWidth = oldRight - oldLeft; 230 final int oldHeight = oldBottom - oldTop; 231 232 if (width != oldWidth || height != oldHeight) { 233 final Rect r = new Rect(); 234 r.right = width; 235 r.bottom = height; 236 urlDisplayScroll.setTouchDelegate(new TouchDelegateWithReset(r, scrollChild)); 237 } 238 } 239 }); 240 241 scrollChild.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { 242 @Override 243 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { 244 // Do not show the context menu while editing 245 if (isEditing()) { 246 return; 247 } 248 249 // NOTE: Use MenuUtils.safeSetVisible because some actions might 250 // be on the Page menu 251 MenuInflater inflater = activity.getMenuInflater(); 252 inflater.inflate(R.menu.titlebar_contextmenu, menu); 253 254 String clipboard = Clipboard.getText(context); 255 if (TextUtils.isEmpty(clipboard)) { 256 menu.findItem(R.id.pasteandgo).setVisible(false); 257 menu.findItem(R.id.paste).setVisible(false); 258 } 259 260 Tab tab = Tabs.getInstance().getSelectedTab(); 261 if (tab != null) { 262 String url = tab.getURL(); 263 if (url == null) { 264 menu.findItem(R.id.copyurl).setVisible(false); 265 MenuUtils.safeSetVisible(menu, R.id.add_to_launcher, false); 266 menu.findItem(R.id.set_as_homepage).setVisible(false); 267 } 268 269 MenuUtils.safeSetVisible(menu, R.id.subscribe, tab.hasFeeds()); 270 MenuUtils.safeSetVisible(menu, R.id.add_search_engine, tab.hasOpenSearch()); 271 final boolean distSetAsHomepage = GeckoSharedPrefs.forProfile(context).getBoolean(GeckoPreferences.PREFS_SET_AS_HOMEPAGE, false); 272 MenuUtils.safeSetVisible(menu, R.id.set_as_homepage, distSetAsHomepage); 273 } else { 274 // if there is no tab, remove anything tab dependent 275 menu.findItem(R.id.copyurl).setVisible(false); 276 MenuUtils.safeSetVisible(menu, R.id.add_to_launcher, false); 277 menu.findItem(R.id.set_as_homepage).setVisible(false); 278 MenuUtils.safeSetVisible(menu, R.id.subscribe, false); 279 MenuUtils.safeSetVisible(menu, R.id.add_search_engine, false); 280 } 281 } 282 }); 283 284 scrollChild.setOnClickListener(new OnClickListener() { 285 @Override 286 public void onClick(View v) { 287 if (activateListener != null) { 288 activateListener.onActivate(); 289 } 290 } 291 }); 292 } 293 294 @Override onAttachedToWindow()295 public void onAttachedToWindow() { 296 super.onAttachedToWindow(); 297 298 prefs.open(); 299 300 urlDisplayLayout.setOnStopListener(new OnStopListener() { 301 @Override 302 public Tab onStop() { 303 final Tab tab = Tabs.getInstance().getSelectedTab(); 304 if (tab != null) { 305 tab.doStop(); 306 return tab; 307 } 308 309 return null; 310 } 311 }); 312 313 urlDisplayLayout.setOnTitleChangeListener(new OnTitleChangeListener() { 314 @Override 315 public void onTitleChange(CharSequence title) { 316 final String contentDescription; 317 if (title != null) { 318 contentDescription = title.toString(); 319 } else { 320 contentDescription = activity.getString(R.string.url_bar_default_text); 321 } 322 323 // The title and content description should 324 // always be sync. 325 setContentDescription(contentDescription); 326 } 327 }); 328 329 urlEditLayout.setOnFocusChangeListener(new View.OnFocusChangeListener() { 330 @Override 331 public void onFocusChange(View v, boolean hasFocus) { 332 // This will select the url bar when entering editing mode. 333 setSelected(hasFocus); 334 if (focusChangeListener != null) { 335 focusChangeListener.onFocusChange(v, hasFocus); 336 } 337 } 338 }); 339 340 tabsButton.setOnClickListener(new Button.OnClickListener() { 341 @Override 342 public void onClick(View v) { 343 // Clear focus so a back press with the tabs 344 // panel open does not go to the editing field. 345 urlEditLayout.clearFocus(); 346 347 toggleTabs(); 348 } 349 }); 350 tabsButton.setImageLevel(0); 351 352 menuButton.setOnClickListener(new Button.OnClickListener() { 353 @Override 354 public void onClick(View view) { 355 // Drop the soft keyboard. 356 urlEditLayout.clearFocus(); 357 activity.openOptionsMenu(); 358 } 359 }); 360 361 WindowUtil.setStatusBarColor(activity, isPrivateMode()); 362 } 363 364 @Override onDetachedFromWindow()365 public void onDetachedFromWindow() { 366 super.onDetachedFromWindow(); 367 368 prefs.close(); 369 } 370 371 @Override draw(Canvas canvas)372 public void draw(Canvas canvas) { 373 super.draw(canvas); 374 375 final int height = getHeight(); 376 canvas.drawRect(0, height - shadowSize, getWidth(), height, shadowPaint); 377 } 378 onParentFocus()379 public void onParentFocus() { 380 urlEditLayout.onParentFocus(); 381 } 382 setProgressBar(AnimatedProgressBar progressBar)383 public void setProgressBar(AnimatedProgressBar progressBar) { 384 this.progressBar = progressBar; 385 } 386 setTabHistoryController(TabHistoryController tabHistoryController)387 public void setTabHistoryController(TabHistoryController tabHistoryController) { 388 this.tabHistoryController = tabHistoryController; 389 } 390 refresh()391 public void refresh() { 392 urlDisplayLayout.dismissSiteIdentityPopup(); 393 urlEditLayout.refresh(); 394 } 395 onBackPressed()396 public boolean onBackPressed() { 397 // If we exit editing mode during the animation, 398 // we're put into an inconsistent state (bug 1017276). 399 if (isEditing() && !isAnimating()) { 400 Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, 401 TelemetryContract.Method.BACK); 402 cancelEdit(); 403 return true; 404 } 405 406 return urlDisplayLayout.dismissSiteIdentityPopup(); 407 } 408 409 @Override onSizeChanged(int w, int h, int oldw, int oldh)410 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 411 super.onSizeChanged(w, h, oldw, oldh); 412 413 if (h != oldh) { 414 // Post this to happen outside of onSizeChanged, as this may cause 415 // a layout change and relayouts within a layout change don't work. 416 post(new Runnable() { 417 @Override 418 public void run() { 419 activity.refreshToolbarHeight(); 420 } 421 }); 422 } 423 } 424 saveTabEditingState(final TabEditingState editingState)425 public void saveTabEditingState(final TabEditingState editingState) { 426 urlEditLayout.saveTabEditingState(editingState); 427 } 428 restoreTabEditingState(final TabEditingState editingState)429 public void restoreTabEditingState(final TabEditingState editingState) { 430 if (!isEditing()) { 431 throw new IllegalStateException("Expected to be editing"); 432 } 433 434 urlEditLayout.restoreTabEditingState(editingState); 435 } 436 437 @Override onTabChanged(@ullable Tab tab, Tabs.TabEvents msg, String data)438 public void onTabChanged(@Nullable Tab tab, Tabs.TabEvents msg, String data) { 439 Log.d(LOGTAG, "onTabChanged: " + msg); 440 final Tabs tabs = Tabs.getInstance(); 441 442 // These conditions are split into three phases: 443 // * Always do first 444 // * Handling specific to the selected tab 445 // * Always do afterwards. 446 447 switch (msg) { 448 case ADDED: 449 case CLOSED: 450 updateTabCount(tabs.getDisplayCount()); 451 break; 452 case RESTORED: 453 // TabCount fixup after OOM 454 case SELECTED: 455 urlDisplayLayout.dismissSiteIdentityPopup(); 456 updateTabCount(tabs.getDisplayCount()); 457 isSwitchingTabs = true; 458 break; 459 } 460 461 if (tabs.isSelectedTab(tab)) { 462 final EnumSet<UpdateFlags> flags = EnumSet.noneOf(UpdateFlags.class); 463 464 // Progress-related handling 465 switch (msg) { 466 case START: 467 flags.add(UpdateFlags.PROGRESS); 468 updateProgressBarState(tab, Tab.LOAD_PROGRESS_INIT); 469 break; 470 471 case LOAD_ERROR: 472 case STOP: 473 if (progressBar.getVisibility() == View.VISIBLE) { 474 // Animate the progress bar to completion before it'll get hidden below. 475 progressBar.setProgress(tab.getLoadProgress()); 476 } 477 // Fall through. 478 case ADDED: 479 case LOCATION_CHANGE: 480 case LOADED: 481 flags.add(UpdateFlags.PROGRESS); 482 updateProgressBarState(); 483 break; 484 485 case SELECTED: 486 flags.add(UpdateFlags.PROGRESS); 487 updateProgressBarState(); 488 break; 489 } 490 491 switch (msg) { 492 case STOP: 493 // Reset the title in case we haven't navigated 494 // to a new page yet. 495 flags.add(UpdateFlags.TITLE); 496 // Fall through. 497 case START: 498 case CLOSED: 499 case ADDED: 500 updateNavigationButtons(tab); 501 break; 502 503 case SELECTED: 504 flags.add(UpdateFlags.PRIVATE_MODE); 505 setPrivateMode(tab.isPrivate()); 506 // Fall through. 507 case LOAD_ERROR: 508 case LOCATION_CHANGE: 509 // We're displaying the tab URL in place of the title, 510 // so we always need to update our "title" here as well. 511 flags.add(UpdateFlags.TITLE); 512 flags.add(UpdateFlags.FAVICON); 513 flags.add(UpdateFlags.SITE_IDENTITY); 514 515 updateNavigationButtons(tab); 516 break; 517 518 case TITLE: 519 flags.add(UpdateFlags.TITLE); 520 break; 521 522 case FAVICON: 523 flags.add(UpdateFlags.FAVICON); 524 break; 525 526 case SECURITY_CHANGE: 527 flags.add(UpdateFlags.SITE_IDENTITY); 528 break; 529 } 530 531 if (!flags.isEmpty() && tab != null) { 532 updateDisplayLayout(tab, flags); 533 } 534 } 535 536 switch (msg) { 537 case SELECTED: 538 case LOAD_ERROR: 539 case LOCATION_CHANGE: 540 isSwitchingTabs = false; 541 } 542 } 543 onLocaleReady(final String locale)544 public void onLocaleReady(final String locale) { 545 final Tabs tabs = Tabs.getInstance(); 546 tabsCounter.setCount(tabs.getDisplayCount()); 547 } 548 549 /** 550 * Updates the progress bar percentage and hides/shows it depending on the loading state of the 551 * currently selected tab. 552 */ updateProgressBarState()553 private void updateProgressBarState() { 554 final Tab selectedTab = Tabs.getInstance().getSelectedTab(); 555 // The selected tab may be null if GeckoApp (and thus the 556 // selected tab) are not yet initialized (bug 1090287). 557 if (selectedTab != null) { 558 updateProgressBarState(selectedTab, selectedTab.getLoadProgress()); 559 } 560 } 561 562 /** 563 * Updates the progress bar to the given <code>progress</code> percentage and hides/shows it 564 * depending on the loading state of the tab passed as <code>selectedTab</code>. 565 */ updateProgressBarState(Tab selectedTab, int progress)566 private void updateProgressBarState(Tab selectedTab, int progress) { 567 if (!isEditing() && selectedTab.getState() == Tab.STATE_LOADING) { 568 progressBar.setProgress(progress); 569 progressBar.setPrivateMode(selectedTab.isPrivate()); 570 progressBar.setVisibility(View.VISIBLE); 571 progressBar.pinDynamicToolbar(); 572 } else { 573 progressBar.setVisibility(View.GONE); 574 progressBar.unpinDynamicToolbar(); 575 } 576 } 577 isVisible()578 protected boolean isVisible() { 579 return ViewHelper.getTranslationY(this) == 0; 580 } 581 582 @Override setNextFocusDownId(int nextId)583 public void setNextFocusDownId(int nextId) { 584 super.setNextFocusDownId(nextId); 585 tabsButton.setNextFocusDownId(nextId); 586 urlDisplayLayout.setNextFocusDownId(nextId); 587 menuButton.setNextFocusDownId(nextId); 588 } 589 hideVirtualKeyboard()590 public boolean hideVirtualKeyboard() { 591 InputMethodManager imm = 592 (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); 593 return imm.hideSoftInputFromWindow(tabsButton.getWindowToken(), 0); 594 } 595 showSelectedTabs()596 private void showSelectedTabs() { 597 Tab tab = Tabs.getInstance().getSelectedTab(); 598 if (tab != null) { 599 if (!tab.isPrivate()) 600 activity.showNormalTabs(); 601 else 602 activity.showPrivateTabs(); 603 } 604 } 605 toggleTabs()606 private void toggleTabs() { 607 if (activity.areTabsShown()) { 608 return; 609 } 610 611 if (hideVirtualKeyboard()) { 612 getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { 613 @Override 614 public void onGlobalLayout() { 615 getViewTreeObserver().removeGlobalOnLayoutListener(this); 616 showSelectedTabs(); 617 } 618 }); 619 } else { 620 showSelectedTabs(); 621 } 622 } 623 updateTabCount(final int count)624 protected void updateTabCount(final int count) { 625 // If toolbar is in edit mode on a phone, this means the entry is expanded 626 // and the tabs button is translated offscreen. Don't trigger tabs counter 627 // updates until the tabs button is back on screen. 628 // See stopEditing() 629 if (isTabsButtonOffscreen()) { 630 return; 631 } 632 633 // Set TabCounter based on visibility 634 if (isVisible() && ViewHelper.getAlpha(tabsCounter) != 0 && !isEditing()) { 635 tabsCounter.setCountWithAnimation(count); 636 } else { 637 tabsCounter.setCount(count); 638 } 639 640 // Update A11y information 641 tabsButton.setContentDescription((count > 1) ? 642 activity.getString(R.string.num_tabs, count) : 643 activity.getString(R.string.one_tab)); 644 } 645 updateDisplayLayout(@onNull Tab tab, EnumSet<UpdateFlags> flags)646 private void updateDisplayLayout(@NonNull Tab tab, EnumSet<UpdateFlags> flags) { 647 if (isSwitchingTabs) { 648 flags.add(UpdateFlags.DISABLE_ANIMATIONS); 649 } 650 651 urlDisplayLayout.updateFromTab(tab, flags); 652 653 if (flags.contains(UpdateFlags.TITLE)) { 654 if (!isEditing()) { 655 urlEditLayout.setText(tab.getURL()); 656 } 657 } 658 659 if (flags.contains(UpdateFlags.PROGRESS)) { 660 updateFocusOrder(); 661 } 662 } 663 updateFocusOrder()664 private void updateFocusOrder() { 665 if (focusOrder.size() == 0) { 666 throw new IllegalStateException("Expected focusOrder to be initialized in subclass"); 667 } 668 669 View prevView = null; 670 671 // If the element that has focus becomes disabled or invisible, focus 672 // is given to the URL bar. 673 boolean needsNewFocus = false; 674 675 for (View view : focusOrder) { 676 if (view.getVisibility() != View.VISIBLE || !view.isEnabled()) { 677 if (view.hasFocus()) { 678 needsNewFocus = true; 679 } 680 continue; 681 } 682 683 if (view.getId() == R.id.menu_items) { 684 final LinearLayout actionItemBar = (LinearLayout) view; 685 final int childCount = actionItemBar.getChildCount(); 686 for (int child = 0; child < childCount; child++) { 687 View childView = actionItemBar.getChildAt(child); 688 if (prevView != null) { 689 childView.setNextFocusLeftId(prevView.getId()); 690 prevView.setNextFocusRightId(childView.getId()); 691 } 692 prevView = childView; 693 } 694 } else { 695 if (prevView != null) { 696 view.setNextFocusLeftId(prevView.getId()); 697 prevView.setNextFocusRightId(view.getId()); 698 } 699 prevView = view; 700 } 701 } 702 703 if (needsNewFocus) { 704 requestFocus(); 705 } 706 } 707 onEditSuggestion(String suggestion)708 public void onEditSuggestion(String suggestion) { 709 if (!isEditing()) { 710 return; 711 } 712 713 urlEditLayout.onEditSuggestion(suggestion); 714 } 715 setTitle(CharSequence title)716 public void setTitle(CharSequence title) { 717 urlDisplayLayout.setTitle(title); 718 } 719 setOnActivateListener(final OnActivateListener listener)720 public void setOnActivateListener(final OnActivateListener listener) { 721 activateListener = listener; 722 } 723 setOnCommitListener(OnCommitListener listener)724 public void setOnCommitListener(OnCommitListener listener) { 725 urlEditLayout.setOnCommitListener(listener); 726 } 727 setOnDismissListener(OnDismissListener listener)728 public void setOnDismissListener(OnDismissListener listener) { 729 urlEditLayout.setOnDismissListener(listener); 730 } 731 setOnFilterListener(OnFilterListener listener)732 public void setOnFilterListener(OnFilterListener listener) { 733 urlEditLayout.setOnFilterListener(listener); 734 } 735 736 @Override setOnFocusChangeListener(OnFocusChangeListener listener)737 public void setOnFocusChangeListener(OnFocusChangeListener listener) { 738 focusChangeListener = listener; 739 } 740 setOnStartEditingListener(OnStartEditingListener listener)741 public void setOnStartEditingListener(OnStartEditingListener listener) { 742 startEditingListener = listener; 743 } 744 setOnStopEditingListener(OnStopEditingListener listener)745 public void setOnStopEditingListener(OnStopEditingListener listener) { 746 stopEditingListener = listener; 747 } 748 showUrlEditLayout()749 protected void showUrlEditLayout() { 750 setUrlEditLayoutVisibility(true, null); 751 } 752 showUrlEditLayout(final PropertyAnimator animator)753 protected void showUrlEditLayout(final PropertyAnimator animator) { 754 setUrlEditLayoutVisibility(true, animator); 755 } 756 hideUrlEditLayout()757 protected void hideUrlEditLayout() { 758 setUrlEditLayoutVisibility(false, null); 759 } 760 hideUrlEditLayout(final PropertyAnimator animator)761 protected void hideUrlEditLayout(final PropertyAnimator animator) { 762 setUrlEditLayoutVisibility(false, animator); 763 } 764 setUrlEditLayoutVisibility(final boolean showEditLayout, PropertyAnimator animator)765 protected void setUrlEditLayoutVisibility(final boolean showEditLayout, PropertyAnimator animator) { 766 if (showEditLayout) { 767 urlEditLayout.prepareShowAnimation(animator); 768 } 769 770 // If this view is GONE, we trigger a measure pass when setting the view to 771 // VISIBLE. Since this will occur during the toolbar open animation, it causes jank. 772 final int hiddenViewVisibility = View.INVISIBLE; 773 774 if (animator == null) { 775 final View viewToShow = (showEditLayout ? urlEditLayout : urlDisplayLayout); 776 final View viewToHide = (showEditLayout ? urlDisplayLayout : urlEditLayout); 777 778 viewToHide.setVisibility(hiddenViewVisibility); 779 viewToShow.setVisibility(View.VISIBLE); 780 return; 781 } 782 783 animator.addPropertyAnimationListener(new PropertyAnimationListener() { 784 @Override 785 public void onPropertyAnimationStart() { 786 if (!showEditLayout) { 787 urlEditLayout.setVisibility(hiddenViewVisibility); 788 urlDisplayLayout.setVisibility(View.VISIBLE); 789 } 790 } 791 792 @Override 793 public void onPropertyAnimationEnd() { 794 if (showEditLayout) { 795 urlDisplayLayout.setVisibility(hiddenViewVisibility); 796 urlEditLayout.setVisibility(View.VISIBLE); 797 } 798 } 799 }); 800 } 801 setUIMode(final UIMode uiMode)802 private void setUIMode(final UIMode uiMode) { 803 this.uiMode = uiMode; 804 urlEditLayout.setEnabled(uiMode == UIMode.EDIT); 805 } 806 807 /** 808 * Returns whether or not the URL bar is in editing mode (url bar is expanded, hiding the new 809 * tab button). Note that selection state is independent of editing mode. 810 */ isEditing()811 public boolean isEditing() { 812 return (uiMode == UIMode.EDIT); 813 } 814 startEditing(String url, PropertyAnimator animator)815 public void startEditing(String url, PropertyAnimator animator) { 816 if (isEditing()) { 817 return; 818 } 819 820 urlEditLayout.setText(url != null ? url : ""); 821 822 setUIMode(UIMode.EDIT); 823 824 updateProgressBarState(); 825 826 if (startEditingListener != null) { 827 startEditingListener.onStartEditing(); 828 } 829 830 triggerStartEditingTransition(animator); 831 } 832 833 /** 834 * Exits edit mode without updating the toolbar title. 835 * 836 * @return the url that was entered 837 */ cancelEdit()838 public String cancelEdit() { 839 Telemetry.stopUISession(TelemetryContract.Session.AWESOMESCREEN); 840 return stopEditing(); 841 } 842 843 /** 844 * Exits edit mode, updating the toolbar title with the url that was just entered. 845 * 846 * @return the url that was entered 847 */ commitEdit()848 public String commitEdit() { 849 Tab tab = Tabs.getInstance().getSelectedTab(); 850 if (tab != null) { 851 tab.resetSiteIdentity(); 852 } 853 854 final String url = stopEditing(); 855 if (!TextUtils.isEmpty(url)) { 856 setTitle(url); 857 } 858 return url; 859 } 860 stopEditing()861 private String stopEditing() { 862 final String url = urlEditLayout.getText(); 863 if (!isEditing()) { 864 return url; 865 } 866 setUIMode(UIMode.DISPLAY); 867 868 if (stopEditingListener != null) { 869 stopEditingListener.onStopEditing(); 870 } 871 872 updateProgressBarState(); 873 triggerStopEditingTransition(); 874 875 return url; 876 } 877 878 @Override setPrivateMode(boolean isPrivate)879 public void setPrivateMode(boolean isPrivate) { 880 final boolean modeChanged = isPrivateMode() != isPrivate; 881 882 super.setPrivateMode(isPrivate); 883 884 tabsButton.setPrivateMode(isPrivate); 885 tabsCounter.setPrivateMode(isPrivate); 886 urlEditLayout.setPrivateMode(isPrivate); 887 urlDisplayLayout.setPrivateMode(isPrivate); 888 889 ((ThemedImageButton) menuButton).setPrivateMode(isPrivate); 890 891 shadowPaint.setColor(isPrivate ? shadowPrivateColor : shadowColor); 892 893 if (modeChanged) { 894 WindowUtil.setStatusBarColor(activity, isPrivate); 895 } 896 } 897 show()898 public void show() { 899 setVisibility(View.VISIBLE); 900 } 901 hide()902 public void hide() { 903 setVisibility(View.GONE); 904 } 905 getDoorHangerAnchor()906 public View getDoorHangerAnchor() { 907 return urlDisplayLayout; 908 } 909 onDestroy()910 public void onDestroy() { 911 Tabs.unregisterOnTabsChangedListener(this); 912 urlDisplayLayout.destroy(); 913 } 914 openOptionsMenu()915 public boolean openOptionsMenu() { 916 // Initialize the popup. 917 if (menuPopup == null) { 918 View panel = activity.getMenuPanel(); 919 menuPopup = new MenuPopup(activity); 920 menuPopup.setPanelView(panel); 921 922 menuPopup.setOnDismissListener(new PopupWindow.OnDismissListener() { 923 @Override 924 public void onDismiss() { 925 activity.onOptionsMenuClosed(null); 926 } 927 }); 928 } 929 930 activity.invalidateOptionsMenu(); 931 if (!menuPopup.isShowing()) { 932 menuPopup.showAsDropDown(menuButton); 933 } 934 935 return true; 936 } 937 closeOptionsMenu()938 public boolean closeOptionsMenu() { 939 if (menuPopup != null && menuPopup.isShowing()) { 940 menuPopup.dismiss(); 941 } 942 943 return true; 944 } 945 946 @Override onLightweightThemeChanged()947 public void onLightweightThemeChanged() { 948 final Drawable drawable = getLWTDefaultStateSetDrawable(); 949 if (drawable == null) { 950 return; 951 } 952 953 final StateListDrawable stateList = new StateListDrawable(); 954 stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.photon_browser_toolbar_bg_private)); 955 stateList.addState(EMPTY_STATE_SET, drawable); 956 957 setBackgroundDrawable(stateList); 958 } 959 setTouchEventInterceptor(TouchEventInterceptor interceptor)960 public void setTouchEventInterceptor(TouchEventInterceptor interceptor) { 961 mTouchEventInterceptor = interceptor; 962 } 963 964 @Override onInterceptTouchEvent(MotionEvent event)965 public boolean onInterceptTouchEvent(MotionEvent event) { 966 if (mTouchEventInterceptor != null && mTouchEventInterceptor.onInterceptTouchEvent(this, event)) { 967 return true; 968 } 969 return super.onInterceptTouchEvent(event); 970 } 971 972 @Override onLightweightThemeReset()973 public void onLightweightThemeReset() { 974 setBackgroundResource(R.drawable.url_bar_bg); 975 } 976 getLightweightThemeDrawable(final View view, final LightweightTheme theme, final int colorResID)977 public static LightweightThemeDrawable getLightweightThemeDrawable(final View view, 978 final LightweightTheme theme, final int colorResID) { 979 final int color = ContextCompat.getColor(view.getContext(), colorResID); 980 981 final LightweightThemeDrawable drawable = theme.getColorDrawable(view, color); 982 if (drawable != null) { 983 final int startAlpha, endAlpha; 984 final boolean horizontalGradient; 985 986 if (HardwareUtils.isTablet()) { 987 startAlpha = LIGHTWEIGHT_THEME_INVERT_ALPHA_TABLET; 988 endAlpha = LIGHTWEIGHT_THEME_INVERT_ALPHA_TABLET; 989 horizontalGradient = false; 990 } else { 991 startAlpha = LIGHTWEIGHT_THEME_INVERT_ALPHA_START; 992 endAlpha = LIGHTWEIGHT_THEME_INVERT_ALPHA_END; 993 horizontalGradient = true; 994 } 995 drawable.setAlpha(startAlpha, endAlpha, horizontalGradient); 996 } 997 998 return drawable; 999 } 1000 1001 public static class TabEditingState { 1002 // The edited text from the most recent time this tab was unselected. 1003 protected String lastEditingText; 1004 protected int selectionStart; 1005 protected int selectionEnd; 1006 1007 public boolean isBrowserSearchShown; 1008 copyFrom(final TabEditingState s2)1009 public void copyFrom(final TabEditingState s2) { 1010 lastEditingText = s2.lastEditingText; 1011 selectionStart = s2.selectionStart; 1012 selectionEnd = s2.selectionEnd; 1013 1014 isBrowserSearchShown = s2.isBrowserSearchShown; 1015 } 1016 isBrowserSearchShown()1017 public boolean isBrowserSearchShown() { 1018 return isBrowserSearchShown; 1019 } 1020 setIsBrowserSearchShown(final boolean isShown)1021 public void setIsBrowserSearchShown(final boolean isShown) { 1022 isBrowserSearchShown = isShown; 1023 } 1024 } 1025 } 1026