1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.keyboard_accessory;
6 
7 import static org.chromium.chrome.browser.flags.ChromeFeatureList.AUTOFILL_KEYBOARD_ACCESSORY;
8 import static org.chromium.chrome.browser.flags.ChromeFeatureList.AUTOFILL_MANUAL_FALLBACK_ANDROID;
9 import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.KEYBOARD_EXTENSION_STATE;
10 import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.EXTENDING_KEYBOARD;
11 import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.FLOATING_BAR;
12 import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.FLOATING_SHEET;
13 import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.HIDDEN;
14 import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.REPLACING_KEYBOARD;
15 import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.WAITING_TO_REPLACE;
16 import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.PORTRAIT_ORIENTATION;
17 import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.SHOW_WHEN_VISIBLE;
18 import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.SUPPRESSED_BY_BOTTOM_SHEET;
19 
20 import android.view.Surface;
21 import android.view.View;
22 import android.view.ViewGroup;
23 
24 import androidx.annotation.Nullable;
25 import androidx.annotation.Px;
26 import androidx.annotation.VisibleForTesting;
27 
28 import org.chromium.base.supplier.ObservableSupplierImpl;
29 import org.chromium.base.supplier.Supplier;
30 import org.chromium.chrome.browser.ChromeKeyboardVisibilityDelegate;
31 import org.chromium.chrome.browser.ChromeWindow;
32 import org.chromium.chrome.browser.app.ChromeActivity;
33 import org.chromium.chrome.browser.compositor.CompositorViewHolder;
34 import org.chromium.chrome.browser.contextualsearch.ContextualSearchManager;
35 import org.chromium.chrome.browser.flags.ChromeFeatureList;
36 import org.chromium.chrome.browser.fullscreen.FullscreenManager;
37 import org.chromium.chrome.browser.fullscreen.FullscreenOptions;
38 import org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState;
39 import org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.StateProperty;
40 import org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryCoordinator;
41 import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData;
42 import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData.Action;
43 import org.chromium.chrome.browser.keyboard_accessory.data.PropertyProvider;
44 import org.chromium.chrome.browser.keyboard_accessory.sheet_component.AccessorySheetCoordinator;
45 import org.chromium.chrome.browser.keyboard_accessory.sheet_tabs.AccessorySheetTabCoordinator;
46 import org.chromium.chrome.browser.keyboard_accessory.sheet_tabs.AddressAccessorySheetCoordinator;
47 import org.chromium.chrome.browser.keyboard_accessory.sheet_tabs.CreditCardAccessorySheetCoordinator;
48 import org.chromium.chrome.browser.keyboard_accessory.sheet_tabs.PasswordAccessorySheetCoordinator;
49 import org.chromium.chrome.browser.keyboard_accessory.sheet_tabs.TouchToFillSheetCoordinator;
50 import org.chromium.chrome.browser.tab.EmptyTabObserver;
51 import org.chromium.chrome.browser.tab.Tab;
52 import org.chromium.chrome.browser.tab.TabHidingType;
53 import org.chromium.chrome.browser.tab.TabObserver;
54 import org.chromium.chrome.browser.tabmodel.TabModelObserver;
55 import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabModelObserver;
56 import org.chromium.chrome.browser.vr.VrModuleProvider;
57 import org.chromium.components.autofill.AutofillDelegate;
58 import org.chromium.components.autofill.AutofillSuggestion;
59 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
60 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.SheetState;
61 import org.chromium.components.browser_ui.bottomsheet.BottomSheetObserver;
62 import org.chromium.components.browser_ui.bottomsheet.EmptyBottomSheetObserver;
63 import org.chromium.components.browser_ui.widget.InsetObserverView;
64 import org.chromium.content_public.browser.WebContents;
65 import org.chromium.ui.DropdownPopupWindow;
66 import org.chromium.ui.base.WindowAndroid;
67 import org.chromium.ui.modelutil.PropertyKey;
68 import org.chromium.ui.modelutil.PropertyModel;
69 import org.chromium.ui.modelutil.PropertyObservable;
70 
71 import java.util.HashSet;
72 
73 /**
74  * This part of the manual filling component manages the state of the manual filling flow depending
75  * on the currently shown tab.
76  */
77 class ManualFillingMediator extends EmptyTabObserver
78         implements KeyboardAccessoryCoordinator.VisibilityDelegate, View.OnLayoutChangeListener {
79     private static final int MINIMAL_AVAILABLE_VERTICAL_SPACE = 128; // in DP.
80     private static final int MINIMAL_AVAILABLE_HORIZONTAL_SPACE = 180; // in DP.
81 
82     private PropertyModel mModel = ManualFillingProperties.createFillingModel();
83     private WindowAndroid mWindowAndroid;
84     private Supplier<InsetObserverView> mInsetObserverViewSupplier;
85     private final ObservableSupplierImpl<Integer> mViewportInsetSupplier =
86             new ObservableSupplierImpl<>();
87     private final ManualFillingStateCache mStateCache = new ManualFillingStateCache();
88     private final HashSet<Tab> mObservedTabs = new HashSet<>();
89     private KeyboardAccessoryCoordinator mKeyboardAccessory;
90     private AccessorySheetCoordinator mAccessorySheet;
91     private ChromeActivity mActivity; // Used to control the keyboard.
92     private TabModelSelectorTabModelObserver mTabModelObserver;
93     private DropdownPopupWindow mPopup;
94     private BottomSheetController mBottomSheetController;
95 
96     private final TabObserver mTabObserver = new EmptyTabObserver() {
97         @Override
98         public void onHidden(Tab tab, @TabHidingType int type) {
99             pause();
100         }
101 
102         @Override
103         public void onDestroyed(Tab tab) {
104             mStateCache.destroyStateFor(tab);
105             pause();
106             refreshTabs();
107         }
108     };
109 
110     private final FullscreenManager.Observer mFullscreenObserver =
111             new FullscreenManager.Observer() {
112                 @Override
113                 public void onEnterFullscreen(Tab tab, FullscreenOptions options) {
114                     pause();
115                 }
116             };
117 
118     private final BottomSheetObserver mBottomSheetObserver = new EmptyBottomSheetObserver() {
119         @Override
120         public void onSheetStateChanged(@SheetState int newState) {
121             mModel.set(SUPPRESSED_BY_BOTTOM_SHEET, newState != SheetState.HIDDEN);
122         }
123     };
124 
125     /** Default constructor */
ManualFillingMediator()126     ManualFillingMediator() {
127         mViewportInsetSupplier.set(0);
128     }
129 
initialize(KeyboardAccessoryCoordinator keyboardAccessory, AccessorySheetCoordinator accessorySheet, WindowAndroid windowAndroid, BottomSheetController sheetController)130     void initialize(KeyboardAccessoryCoordinator keyboardAccessory,
131             AccessorySheetCoordinator accessorySheet, WindowAndroid windowAndroid,
132             BottomSheetController sheetController) {
133         mActivity = (ChromeActivity) windowAndroid.getActivity().get();
134         assert mActivity != null;
135         mWindowAndroid = windowAndroid;
136         mWindowAndroid.getApplicationBottomInsetProvider().addSupplier(mViewportInsetSupplier);
137         mKeyboardAccessory = keyboardAccessory;
138         mBottomSheetController = sheetController;
139         mModel.set(PORTRAIT_ORIENTATION, hasPortraitOrientation());
140         mModel.addObserver(this::onPropertyChanged);
141         mAccessorySheet = accessorySheet;
142         mAccessorySheet.setOnPageChangeListener(mKeyboardAccessory.getOnPageChangeListener());
143         mAccessorySheet.setHeight(3
144                 * mActivity.getResources().getDimensionPixelSize(
145                         R.dimen.keyboard_accessory_suggestion_height));
146         setInsetObserverViewSupplier(mActivity::getInsetObserverView);
147         mActivity.findViewById(android.R.id.content).addOnLayoutChangeListener(this);
148         mTabModelObserver = new TabModelSelectorTabModelObserver(mActivity.getTabModelSelector()) {
149             @Override
150             public void didSelectTab(Tab tab, int type, int lastId) {
151                 ensureObserverRegistered(tab);
152                 refreshTabs();
153             }
154 
155             @Override
156             public void tabClosureCommitted(Tab tab) {
157                 super.tabClosureCommitted(tab);
158                 mObservedTabs.remove(tab);
159                 tab.removeObserver(mTabObserver); // Fails silently if observer isn't registered.
160                 mStateCache.destroyStateFor(tab);
161             }
162         };
163         mActivity.getFullscreenManager().addObserver(mFullscreenObserver);
164         mBottomSheetController.addObserver(mBottomSheetObserver);
165         ensureObserverRegistered(getActiveBrowserTab());
166         refreshTabs();
167     }
168 
isInitialized()169     boolean isInitialized() {
170         return mWindowAndroid != null;
171     }
172 
isFillingViewShown(View view)173     boolean isFillingViewShown(View view) {
174         return isInitialized() && !isSoftKeyboardShowing(view) && mKeyboardAccessory.hasActiveTab();
175     }
176 
177     @Override
onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)178     public void onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft,
179             int oldTop, int oldRight, int oldBottom) {
180         if (!isInitialized()) return; // Activity uninitialized or cleaned up already.
181         if (mKeyboardAccessory.empty()) return; // Exit early to not affect the layout.
182         if (!hasSufficientSpace()) {
183             mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
184             return;
185         }
186         if (hasPortraitOrientation() != mModel.get(PORTRAIT_ORIENTATION)) {
187             mModel.set(PORTRAIT_ORIENTATION, hasPortraitOrientation());
188             return;
189         }
190         restrictAccessorySheetHeight();
191         if (!isSoftKeyboardShowing(view)) {
192             if (is(WAITING_TO_REPLACE)) mModel.set(KEYBOARD_EXTENSION_STATE, REPLACING_KEYBOARD);
193             if (is(EXTENDING_KEYBOARD)) mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
194             // Cancel animations if the keyboard suddenly closes so the bar doesn't linger.
195             if (is(HIDDEN)) mKeyboardAccessory.skipClosingAnimationOnce();
196             // Layout changes when entering/resizing/leaving MultiWindow. Ensure a consistent state:
197             updateKeyboard(mModel.get(KEYBOARD_EXTENSION_STATE));
198             return;
199         }
200         if (is(WAITING_TO_REPLACE)) return;
201         mModel.set(KEYBOARD_EXTENSION_STATE,
202                 mModel.get(SHOW_WHEN_VISIBLE) ? EXTENDING_KEYBOARD : HIDDEN);
203     }
204 
hasPortraitOrientation()205     private boolean hasPortraitOrientation() {
206         return mWindowAndroid.getDisplay().getRotation() == Surface.ROTATION_0
207                 || mWindowAndroid.getDisplay().getRotation() == Surface.ROTATION_180;
208     }
209 
registerSheetDataProvider(@ccessoryTabType int tabType, PropertyProvider<KeyboardAccessoryData.AccessorySheetData> dataProvider)210     void registerSheetDataProvider(@AccessoryTabType int tabType,
211             PropertyProvider<KeyboardAccessoryData.AccessorySheetData> dataProvider) {
212         if (!isInitialized()) return;
213         ManualFillingState state = mStateCache.getStateFor(mActivity.getCurrentWebContents());
214 
215         state.wrapSheetDataProvider(tabType, dataProvider);
216         AccessorySheetTabCoordinator accessorySheet = getOrCreateSheet(tabType);
217         if (accessorySheet == null) return; // Not available or initialized yet.
218         accessorySheet.registerDataProvider(state.getSheetDataProvider(tabType));
219     }
220 
registerAutofillProvider( PropertyProvider<AutofillSuggestion[]> autofillProvider, AutofillDelegate delegate)221     void registerAutofillProvider(
222             PropertyProvider<AutofillSuggestion[]> autofillProvider, AutofillDelegate delegate) {
223         if (!isInitialized()) return;
224         if (mKeyboardAccessory == null) return;
225         mKeyboardAccessory.registerAutofillProvider(autofillProvider, delegate);
226     }
227 
registerActionProvider(PropertyProvider<Action[]> actionProvider)228     void registerActionProvider(PropertyProvider<Action[]> actionProvider) {
229         if (!isInitialized()) return;
230         ManualFillingState state = mStateCache.getStateFor(mActivity.getCurrentWebContents());
231 
232         state.wrapActionsProvider(actionProvider, new Action[0]);
233         mKeyboardAccessory.registerActionProvider(state.getActionsProvider());
234     }
235 
destroy()236     void destroy() {
237         if (!isInitialized()) return;
238         pause();
239         mWindowAndroid.getApplicationBottomInsetProvider().removeSupplier(mViewportInsetSupplier);
240         mActivity.findViewById(android.R.id.content).removeOnLayoutChangeListener(this);
241         mTabModelObserver.destroy();
242         mStateCache.destroy();
243         for (Tab tab : mObservedTabs) tab.removeObserver(mTabObserver);
244         mObservedTabs.clear();
245         mActivity.getFullscreenManager().removeObserver(mFullscreenObserver);
246         mBottomSheetController.removeObserver(mBottomSheetObserver);
247         mWindowAndroid = null;
248         mActivity = null;
249     }
250 
handleBackPress()251     boolean handleBackPress() {
252         if (isInitialized()
253                 && (is(WAITING_TO_REPLACE) || is(REPLACING_KEYBOARD) || is(FLOATING_SHEET))) {
254             pause();
255             return true;
256         }
257         return false;
258     }
259 
dismiss()260     void dismiss() {
261         if (!isInitialized()) return;
262         pause();
263         ViewGroup contentView = getContentView();
264         if (contentView != null) getKeyboard().hideSoftKeyboardOnly(contentView);
265     }
266 
notifyPopupOpened(DropdownPopupWindow popup)267     void notifyPopupOpened(DropdownPopupWindow popup) {
268         mPopup = popup;
269     }
270 
showWhenKeyboardIsVisible()271     void showWhenKeyboardIsVisible() {
272         if (!isInitialized()) return;
273         mModel.set(SHOW_WHEN_VISIBLE, true);
274         if (is(HIDDEN)) mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_BAR);
275     }
276 
hide()277     void hide() {
278         mModel.set(SHOW_WHEN_VISIBLE, false);
279         if (!isInitialized()) return;
280         mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
281     }
282 
pause()283     void pause() {
284         if (!isInitialized()) return;
285         // When pause is called, the accessory needs to disappear fast since some UI forced it to
286         // close (e.g. a scene changed or the screen was turned off).
287         mKeyboardAccessory.skipClosingAnimationOnce();
288         mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
289     }
290 
onOrientationChange()291     private void onOrientationChange() {
292         if (!isInitialized()) return;
293         if (ChromeFeatureList.isEnabled(AUTOFILL_KEYBOARD_ACCESSORY) || is(REPLACING_KEYBOARD)
294                 || is(FLOATING_SHEET)) {
295             mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
296             // Autofill suggestions are invalidated on rotation. Dismissing all filling UI forces
297             // the user to interact with the field they want to edit. This refreshes Autofill.
298             if (ChromeFeatureList.isEnabled(AUTOFILL_KEYBOARD_ACCESSORY)) {
299                 hideSoftKeyboard();
300             }
301         }
302     }
303 
resume()304     void resume() {
305         if (!isInitialized()) return;
306         pause(); // Resuming dismisses the keyboard. Ensure the accessory doesn't linger.
307         refreshTabs();
308     }
309 
hasSufficientSpace()310     private boolean hasSufficientSpace() {
311         if (mActivity == null) return false;
312         WebContents webContents = mActivity.getCurrentWebContents();
313         if (webContents == null) return false;
314         float height = webContents.getHeight(); // getHeight actually returns dip, not Px!
315         height += mViewportInsetSupplier.get() / mWindowAndroid.getDisplay().getDipScale();
316         return height >= MINIMAL_AVAILABLE_VERTICAL_SPACE
317                 && webContents.getWidth() >= MINIMAL_AVAILABLE_HORIZONTAL_SPACE;
318     }
319 
onPropertyChanged(PropertyObservable<PropertyKey> source, PropertyKey property)320     private void onPropertyChanged(PropertyObservable<PropertyKey> source, PropertyKey property) {
321         assert source == mModel;
322         if (property == SHOW_WHEN_VISIBLE) {
323             return;
324         } else if (property == PORTRAIT_ORIENTATION) {
325             onOrientationChange();
326             return;
327         } else if (property == KEYBOARD_EXTENSION_STATE) {
328             transitionIntoState(mModel.get(KEYBOARD_EXTENSION_STATE));
329             return;
330         } else if (property == SUPPRESSED_BY_BOTTOM_SHEET) {
331             if (isInitialized() && mModel.get(SUPPRESSED_BY_BOTTOM_SHEET)) {
332                 mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
333             }
334             return;
335         }
336         throw new IllegalArgumentException("Unhandled property: " + property);
337     }
338 
339     /**
340      * If preconditions for a state are met, enforce the state's properties and trigger its effects.
341      * @param extensionState The {@link KeyboardExtensionState} to transition into.
342      */
transitionIntoState(@eyboardExtensionState int extensionState)343     private void transitionIntoState(@KeyboardExtensionState int extensionState) {
344         if (!meetsStatePreconditions(extensionState)) return;
345         enforceStateProperties(extensionState);
346         changeBottomControlSpaceForState(extensionState);
347         updateKeyboard(extensionState);
348     }
349 
350     /**
351      * Checks preconditions for states and redirects to a different state if they are not met.
352      * @param extensionState The {@link KeyboardExtensionState} to transition into.
353      */
meetsStatePreconditions(@eyboardExtensionState int extensionState)354     private boolean meetsStatePreconditions(@KeyboardExtensionState int extensionState) {
355         switch (extensionState) {
356             case HIDDEN:
357                 return true;
358             case FLOATING_BAR:
359                 if (isSoftKeyboardShowing(getContentView())) {
360                     mModel.set(KEYBOARD_EXTENSION_STATE, EXTENDING_KEYBOARD);
361                     return false;
362                 }
363                 // Intentional fallthrough.
364             case EXTENDING_KEYBOARD:
365                 if (!canExtendKeyboard() || mModel.get(SUPPRESSED_BY_BOTTOM_SHEET)) {
366                     mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
367                     return false;
368                 }
369                 return true;
370             case FLOATING_SHEET:
371                 if (isSoftKeyboardShowing(getContentView())) {
372                     mModel.set(KEYBOARD_EXTENSION_STATE, EXTENDING_KEYBOARD);
373                     return false;
374                 }
375                 // Intentional fallthrough.
376             case REPLACING_KEYBOARD:
377                 if (isSoftKeyboardShowing(getContentView())) {
378                     mModel.set(KEYBOARD_EXTENSION_STATE, WAITING_TO_REPLACE);
379                     return false; // Wait for the keyboard to disappear before replacing!
380                 }
381                 // Intentional fallthrough.
382             case WAITING_TO_REPLACE:
383                 if (!hasSufficientSpace() || mModel.get(SUPPRESSED_BY_BOTTOM_SHEET)) {
384                     mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
385                     return false;
386                 }
387                 return true;
388         }
389         throw new IllegalArgumentException(
390                 "Unhandled transition into state: " + mModel.get(KEYBOARD_EXTENSION_STATE));
391     }
392 
enforceStateProperties(@eyboardExtensionState int extensionState)393     private void enforceStateProperties(@KeyboardExtensionState int extensionState) {
394         if (requiresVisibleBar(extensionState)) {
395             mKeyboardAccessory.show();
396         } else {
397             mKeyboardAccessory.dismiss();
398         }
399         if (extensionState == EXTENDING_KEYBOARD) mKeyboardAccessory.prepareUserEducation();
400         if (requiresVisibleSheet(extensionState)) {
401             mAccessorySheet.show();
402             // TODO(crbug.com/853768): Enable animation that works with sheet (if possible).
403             mKeyboardAccessory.skipClosingAnimationOnce();
404         } else if (requiresHiddenSheet(extensionState)) {
405             mKeyboardAccessory.closeActiveTab();
406             mAccessorySheet.hide();
407             // The compositor should relayout the view when the sheet is hidden. This is necessary
408             // to trigger events that rely on the relayout (like toggling the overview button):
409             CompositorViewHolder compositorViewHolder = mActivity.getCompositorViewHolder();
410             if (compositorViewHolder != null) {
411                 // The CompositorViewHolder is null when the activity is in the process of being
412                 // destroyed which also renders relayouting pointless.
413                 compositorViewHolder.requestLayout();
414             }
415         }
416     }
417 
updateKeyboard(@eyboardExtensionState int extensionState)418     private void updateKeyboard(@KeyboardExtensionState int extensionState) {
419         if (isFloating(extensionState)) {
420             // Keyboard-bound states are always preferable over floating states. Therefore, trigger
421             // a keyboard here. This also allows for smooth transitions, e.g. when closing a sheet:
422             // the REPLACING state transitions into FLOATING_SHEET which triggers the keyboard which
423             // transitions into the EXTENDING state as soon as the keyboard appeared.
424             ViewGroup contentView = getContentView();
425             if (contentView != null) getKeyboard().showKeyboard(contentView);
426         } else if (extensionState == WAITING_TO_REPLACE) {
427             // In order to give the keyboard time to disappear, hide the keyboard and enter the
428             // REPLACING state.
429             hideSoftKeyboard();
430         }
431     }
432 
hideSoftKeyboard()433     private void hideSoftKeyboard() {
434         // If there is a keyboard, update the accessory sheet's height and hide the keyboard.
435         ViewGroup contentView = getContentView();
436         if (contentView == null) return; // Apparently the tab was cleaned up already.
437         View rootView = contentView.getRootView();
438         if (rootView == null) return;
439         mAccessorySheet.setHeight(calculateAccessorySheetHeight(rootView));
440         getKeyboard().hideSoftKeyboardOnly(rootView);
441     }
442 
443     /**
444      * Returns whether the accessory bar can be shown.
445      * @return True if the keyboard can (and should) be shown. False otherwise.
446      */
canExtendKeyboard()447     private boolean canExtendKeyboard() {
448         if (!mModel.get(SHOW_WHEN_VISIBLE)) return false;
449 
450         // When in VR mode, don't extend the keyboard
451         if (VrModuleProvider.getDelegate().isInVr()) return false;
452 
453         // Don't open the accessory inside the contextual search panel.
454         ContextualSearchManager contextualSearch = mActivity.getContextualSearchManager();
455         if (contextualSearch != null && contextualSearch.isSearchPanelOpened()) return false;
456 
457         // If an accessory sheet was opened, the accessory bar must be visible.
458         if (mAccessorySheet.isShown()) return true;
459 
460         return hasSufficientSpace(); // Only extend the keyboard, if there is enough space.
461     }
462 
463     @Override
onChangeAccessorySheet(int tabIndex)464     public void onChangeAccessorySheet(int tabIndex) {
465         if (!isInitialized()) return;
466         mAccessorySheet.setActiveTab(tabIndex);
467         if (mPopup != null && mPopup.isShowing()) mPopup.dismiss();
468         if (is(EXTENDING_KEYBOARD)) {
469             mModel.set(KEYBOARD_EXTENSION_STATE, REPLACING_KEYBOARD);
470         } else if (is(FLOATING_BAR)) {
471             mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_SHEET);
472         }
473     }
474 
475     @Override
onCloseAccessorySheet()476     public void onCloseAccessorySheet() {
477         if (is(REPLACING_KEYBOARD) || is(WAITING_TO_REPLACE)) {
478             mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_SHEET);
479         } else if (is(FLOATING_SHEET)) {
480             mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_BAR);
481         }
482     }
483 
484     /**
485      * Opens the keyboard which implicitly dismisses the sheet. Without open sheet, this is a NoOp.
486      */
swapSheetWithKeyboard()487     void swapSheetWithKeyboard() {
488         if (isInitialized() && mAccessorySheet.isShown()) onCloseAccessorySheet();
489     }
490 
changeBottomControlSpaceForState(int extensionState)491     private void changeBottomControlSpaceForState(int extensionState) {
492         if (extensionState == WAITING_TO_REPLACE) return; // Don't change yet.
493         int newControlsHeight = 0;
494         int newControlsOffset = 0;
495         if (requiresVisibleBar(extensionState)) {
496             newControlsHeight = mActivity.getResources().getDimensionPixelSize(
497                     R.dimen.keyboard_accessory_suggestion_height);
498         }
499         if (requiresVisibleSheet(extensionState)) {
500             newControlsHeight += mAccessorySheet.getHeight();
501             newControlsOffset += mAccessorySheet.getHeight();
502         }
503         mKeyboardAccessory.setBottomOffset(newControlsOffset);
504         mViewportInsetSupplier.set(newControlsHeight);
505     }
506 
507     /**
508      * When trying to get the content of the active tab, there are several cases where a component
509      * can be null - usually use before initialization or after destruction.
510      * This helper ensures that the IDE warns about unchecked use of the all Nullable methods and
511      * provides a shorthand for checking that all components are ready to use.
512      * @return The content {@link View} of the held {@link ChromeActivity} or null if any part of it
513      *         isn't ready to use.
514      */
getContentView()515     private @Nullable ViewGroup getContentView() {
516         if (mActivity == null) return null;
517         Tab tab = getActiveBrowserTab();
518         if (tab == null) return null;
519         return tab.getContentView();
520     }
521 
522     /**
523      * Shorthand to get the activity tab.
524      * @return The currently visible {@link Tab}, if any.
525      */
getActiveBrowserTab()526     private @Nullable Tab getActiveBrowserTab() {
527         return mActivity.getActivityTabProvider().get();
528     }
529 
530     /**
531      * Registers a {@link TabObserver} to the given {@link Tab} if it hasn't been done yet.
532      * Using this function avoid deleting and readding the observer (each O(N)) since the tab does
533      * not report whether an observer is registered.
534      * @param tab A {@link Tab}. May be the currently active tab which is allowed to be null.
535      */
ensureObserverRegistered(@ullable Tab tab)536     private void ensureObserverRegistered(@Nullable Tab tab) {
537         if (tab == null) return; // No tab given, no observer necessary.
538         if (!mObservedTabs.add(tab)) return; // Observer already registered.
539         tab.addObserver(mTabObserver);
540     }
541 
getKeyboard()542     private ChromeKeyboardVisibilityDelegate getKeyboard() {
543         assert mWindowAndroid instanceof ChromeWindow;
544         assert mWindowAndroid.getKeyboardDelegate() instanceof ChromeKeyboardVisibilityDelegate;
545         return (ChromeKeyboardVisibilityDelegate) mWindowAndroid.getKeyboardDelegate();
546     }
547 
isSoftKeyboardShowing(@ullable View view)548     private boolean isSoftKeyboardShowing(@Nullable View view) {
549         return view != null && getKeyboard().isSoftKeyboardShowing(mActivity, view);
550     }
551 
552     /**
553      * Uses the keyboard (if available) to determine the height of the accessory sheet.
554      * @param rootView Root view of the current content view -- used to estimate the height unless
555      *                 the more reliable InsetObserver is available.
556      * @return The estimated keyboard height or enough space to display at least three suggestions.
557      */
calculateAccessorySheetHeight(View rootView)558     private @Px int calculateAccessorySheetHeight(View rootView) {
559         InsetObserverView insetObserver = mInsetObserverViewSupplier.get();
560         int minimalSheetHeight = 3
561                 * mActivity.getResources().getDimensionPixelSize(
562                         R.dimen.keyboard_accessory_suggestion_height);
563         int newSheetHeight = insetObserver != null
564                 ? insetObserver.getSystemWindowInsetsBottom()
565                 : getKeyboard().calculateKeyboardHeight(rootView);
566         newSheetHeight = Math.max(minimalSheetHeight, newSheetHeight);
567         return newSheetHeight;
568     }
569 
570     /**
571      * Double-checks that the accessory sheet height doesn't cover the whole page.
572      */
restrictAccessorySheetHeight()573     private void restrictAccessorySheetHeight() {
574         if (!is(FLOATING_SHEET) && !is(REPLACING_KEYBOARD)) return;
575         WebContents webContents = mActivity.getCurrentWebContents();
576         if (webContents == null) return;
577         float density = mWindowAndroid.getDisplay().getDipScale();
578         // The maximal height for the sheet ensures a minimal amount of WebContents space.
579         @Px
580         int maxHeight = mViewportInsetSupplier.get();
581         maxHeight += Math.round(density * webContents.getHeight());
582         maxHeight -= Math.round(density * MINIMAL_AVAILABLE_VERTICAL_SPACE);
583         if (mAccessorySheet.getHeight() <= maxHeight) return; // Sheet height needs no adjustment!
584         mAccessorySheet.setHeight(maxHeight);
585         changeBottomControlSpaceForState(mModel.get(KEYBOARD_EXTENSION_STATE));
586     }
587 
refreshTabs()588     private void refreshTabs() {
589         if (!isInitialized()) return;
590         ManualFillingState state = mStateCache.getStateFor(mActivity.getCurrentWebContents());
591         state.notifyObservers();
592         KeyboardAccessoryData.Tab[] tabs = state.getTabs();
593         mAccessorySheet.setTabs(tabs); // Set the sheet tabs first to invalidate the tabs properly.
594         mKeyboardAccessory.setTabs(tabs);
595     }
596 
597     @VisibleForTesting
getOrCreateSheet(@ccessoryTabType int tabType)598     AccessorySheetTabCoordinator getOrCreateSheet(@AccessoryTabType int tabType) {
599         if (!canCreateSheet(tabType)) return null;
600         WebContents webContents = mActivity.getCurrentWebContents();
601         if (webContents == null) return null; // There is no active tab or it's being destroyed.
602         ManualFillingState state = mStateCache.getStateFor(webContents);
603         if (state.getAccessorySheet(tabType) != null) return state.getAccessorySheet(tabType);
604 
605         AccessorySheetTabCoordinator sheet = createNewSheet(tabType);
606         assert sheet != null : "Cannot create sheet for type " + tabType;
607 
608         state.setAccessorySheet(tabType, sheet);
609         if (state.getSheetDataProvider(tabType) != null) {
610             sheet.registerDataProvider(state.getSheetDataProvider(tabType));
611         }
612         refreshTabs();
613         return sheet;
614     }
615 
canCreateSheet(@ccessoryTabType int tabType)616     private boolean canCreateSheet(@AccessoryTabType int tabType) {
617         if (!isInitialized()) return false;
618         switch (tabType) {
619             case AccessoryTabType.ALL: // Intentional fallthrough.
620             case AccessoryTabType.COUNT:
621                 return false;
622             case AccessoryTabType.CREDIT_CARDS: // Intentional fallthrough.
623             case AccessoryTabType.ADDRESSES:
624                 return ChromeFeatureList.isEnabled(AUTOFILL_MANUAL_FALLBACK_ANDROID);
625             case AccessoryTabType.PASSWORDS:
626                 return true;
627             case AccessoryTabType.TOUCH_TO_FILL:
628                 return true;
629         }
630         return true;
631     }
632 
createNewSheet(@ccessoryTabType int tabType)633     private AccessorySheetTabCoordinator createNewSheet(@AccessoryTabType int tabType) {
634         switch (tabType) {
635             case AccessoryTabType.CREDIT_CARDS:
636                 return new CreditCardAccessorySheetCoordinator(
637                         mActivity, mAccessorySheet.getScrollListener());
638             case AccessoryTabType.ADDRESSES:
639                 return new AddressAccessorySheetCoordinator(
640                         mActivity, mAccessorySheet.getScrollListener());
641             case AccessoryTabType.PASSWORDS:
642                 return new PasswordAccessorySheetCoordinator(
643                         mActivity, mAccessorySheet.getScrollListener());
644             case AccessoryTabType.TOUCH_TO_FILL:
645                 return new TouchToFillSheetCoordinator(
646                         mActivity, mAccessorySheet.getScrollListener());
647             case AccessoryTabType.ALL: // Intentional fallthrough.
648             case AccessoryTabType.COUNT: // Intentional fallthrough.
649         }
650         return null;
651     }
652 
isFloating(@eyboardExtensionState int state)653     private boolean isFloating(@KeyboardExtensionState int state) {
654         return (state & StateProperty.FLOATING) != 0;
655     }
656 
requiresVisibleBar(@eyboardExtensionState int state)657     private boolean requiresVisibleBar(@KeyboardExtensionState int state) {
658         return (state & StateProperty.BAR) != 0;
659     }
660 
requiresVisibleSheet(@eyboardExtensionState int state)661     private boolean requiresVisibleSheet(@KeyboardExtensionState int state) {
662         return (state & StateProperty.VISIBLE_SHEET) != 0;
663     }
664 
requiresHiddenSheet(int state)665     private boolean requiresHiddenSheet(int state) {
666         return (state & StateProperty.HIDDEN_SHEET) != 0;
667     }
668 
is(@eyboardExtensionState int state)669     private boolean is(@KeyboardExtensionState int state) {
670         return mModel.get(KEYBOARD_EXTENSION_STATE) == state;
671     }
672 
673     @VisibleForTesting
setInsetObserverViewSupplier(Supplier<InsetObserverView> insetObserverViewSupplier)674     void setInsetObserverViewSupplier(Supplier<InsetObserverView> insetObserverViewSupplier) {
675         mInsetObserverViewSupplier = insetObserverViewSupplier;
676     }
677 
678     @VisibleForTesting
getTabModelObserverForTesting()679     TabModelObserver getTabModelObserverForTesting() {
680         return mTabModelObserver;
681     }
682 
683     @VisibleForTesting
getTabObserverForTesting()684     TabObserver getTabObserverForTesting() {
685         return mTabObserver;
686     }
687 
688     @VisibleForTesting
getStateCacheForTesting()689     ManualFillingStateCache getStateCacheForTesting() {
690         return mStateCache;
691     }
692 
693     @VisibleForTesting
getModelForTesting()694     PropertyModel getModelForTesting() {
695         return mModel;
696     }
697 
698     @VisibleForTesting
getKeyboardAccessory()699     KeyboardAccessoryCoordinator getKeyboardAccessory() {
700         return mKeyboardAccessory;
701     }
702 }
703