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