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