| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.chrome.browser.autofill_assistant; |
| |
| import android.content.Context; |
| |
| import androidx.annotation.Nullable; |
| |
| import org.chromium.base.annotations.CalledByNative; |
| import org.chromium.base.annotations.JNINamespace; |
| import org.chromium.base.annotations.NativeMethods; |
| import org.chromium.base.task.PostTask; |
| import org.chromium.chrome.autofill_assistant.R; |
| import org.chromium.chrome.browser.ActivityTabProvider; |
| import org.chromium.chrome.browser.app.ChromeActivity; |
| import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantChip; |
| import org.chromium.chrome.browser.autofill_assistant.metrics.DropOutReason; |
| import org.chromium.chrome.browser.customtabs.CustomTabActivity; |
| import org.chromium.chrome.browser.profiles.Profile; |
| import org.chromium.chrome.browser.tab.Tab; |
| import org.chromium.chrome.browser.ui.TabObscuringHandler; |
| import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager.SnackbarController; |
| import org.chromium.components.browser_ui.bottomsheet.BottomSheetController; |
| import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.SheetState; |
| import org.chromium.components.browser_ui.bottomsheet.BottomSheetControllerProvider; |
| import org.chromium.content_public.browser.UiThreadTaskTraits; |
| import org.chromium.content_public.browser.WebContents; |
| import org.chromium.ui.base.WindowAndroid; |
| import org.chromium.ui.modelutil.PropertyKey; |
| import org.chromium.ui.modelutil.PropertyObservable; |
| import org.chromium.ui.modelutil.PropertyObservable.PropertyObserver; |
| |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * Bridge to native side autofill_assistant::UiControllerAndroid. It allows native side to control |
| * Autofill Assistant related UIs and forward UI events to native side. |
| * This controller is purely a translation and forwarding layer between Native side and the |
| * different Java coordinators. |
| */ |
| @JNINamespace("autofill_assistant") |
| // TODO(crbug.com/806868): This class should be removed once all logic is in native side and the |
| // model is directly modified by the native AssistantMediator. |
| public class AutofillAssistantUiController { |
| private static Set<ChromeActivity> sActiveChromeActivities; |
| private long mNativeUiController; |
| |
| private final ChromeActivity mActivity; |
| private final AssistantCoordinator mCoordinator; |
| private final ActivityTabProvider.ActivityTabTabObserver mActivityTabObserver; |
| private WebContents mWebContents; |
| private SnackbarController mSnackbarController; |
| |
| /** |
| * Getter for the current profile while assistant is running. Since autofill assistant is only |
| * available in regular mode and there is only one regular profile in android, this method |
| * returns {@link Profile#getLastUsedRegularProfile()}. |
| * |
| * TODO(b/161519639): Return current profile to support multi profiles, instead of returning |
| * always regular profile. This could be achieve by retrieving profile from native and using it |
| * where the profile is needed on Java side. |
| * @return The current regular profile. |
| */ |
| public static Profile getProfile() { |
| return Profile.getLastUsedRegularProfile(); |
| } |
| /** |
| * Finds an activity to which a AA UI can be added. |
| * |
| * <p>The activity must not already have an AA UI installed. |
| */ |
| @CalledByNative |
| @Nullable |
| private static ChromeActivity findAppropriateActivity(WebContents webContents) { |
| ChromeActivity activity = ChromeActivity.fromWebContents(webContents); |
| if (activity != null && isActive(activity)) { |
| return null; |
| } |
| |
| return activity; |
| } |
| |
| /** |
| * Returns {@code true} if an AA UI is active on the given activity. |
| * |
| * <p>Used to avoid creating duplicate coordinators views. |
| * |
| * <p>TODO(crbug.com/806868): Refactor to have AssistantCoordinator owned by the activity, so |
| * it's easy to guarantee that there will be at most one per activity. |
| */ |
| private static boolean isActive(ChromeActivity activity) { |
| if (sActiveChromeActivities == null) { |
| return false; |
| } |
| |
| return sActiveChromeActivities.contains(activity); |
| } |
| |
| @CalledByNative |
| private static AutofillAssistantUiController create(ChromeActivity activity, |
| boolean allowTabSwitching, long nativeUiController, |
| @Nullable AssistantOnboardingCoordinator onboardingCoordinator) { |
| BottomSheetController sheetController = |
| BottomSheetControllerProvider.from(activity.getWindowAndroid()); |
| assert activity != null; |
| assert sheetController != null; |
| |
| if (sActiveChromeActivities == null) { |
| sActiveChromeActivities = new HashSet<>(); |
| } |
| sActiveChromeActivities.add(activity); |
| |
| // TODO(crbug.com/1048983): Have the params be passed in to the constructor directly rather |
| // than obtaining them from ChromeActivity getters. |
| return new AutofillAssistantUiController(activity, sheetController, |
| activity.getTabObscuringHandler(), allowTabSwitching, nativeUiController, |
| onboardingCoordinator); |
| } |
| |
| private AutofillAssistantUiController(ChromeActivity activity, BottomSheetController controller, |
| TabObscuringHandler tabObscuringHandler, boolean allowTabSwitching, |
| long nativeUiController, |
| @Nullable AssistantOnboardingCoordinator onboardingCoordinator) { |
| mNativeUiController = nativeUiController; |
| mActivity = activity; |
| mCoordinator = new AssistantCoordinator(activity, controller, tabObscuringHandler, |
| onboardingCoordinator == null ? null : onboardingCoordinator.transferControls(), |
| this::safeNativeOnKeyboardVisibilityChanged); |
| mActivityTabObserver = |
| new ActivityTabProvider.ActivityTabTabObserver( |
| activity.getActivityTabProvider(), /* shouldTrigger = */ true) { |
| @Override |
| protected void onObservingDifferentTab(Tab tab, boolean hint) { |
| if (mWebContents == null) { |
| if (!hint) { |
| // This particular scenario would happen only if we're switching |
| // from a tab with no Autofill Assistant running to a tab with AA |
| // running with no tab switching hinting (i.e. a first notification |
| // with |hint| set to true). |
| // In this case the native side is not yet fully initialized, so we |
| // need to wait for the web contents to be set from native before |
| // notifying native that the tab was selected. |
| setWebContentObserver(tab); |
| } |
| return; |
| } |
| |
| if (!allowTabSwitching) { |
| if (tab == null || tab.getWebContents() != mWebContents) { |
| safeNativeOnFatalError( |
| activity.getString(R.string.autofill_assistant_give_up), |
| DropOutReason.TAB_CHANGED); |
| } |
| return; |
| } |
| |
| // Get rid of any undo snackbars right away before switching tabs, to avoid |
| // confusion. |
| dismissSnackbar(); |
| |
| if (tab == null) { |
| safeOnTabSwitched(getModel().getBottomSheetState(), |
| /* activityChanged = */ false); |
| // A null tab indicates that there's no selected tab; Most likely, we're |
| // in the process of selecting a new tab. Hide the UI for possible reuse |
| // later. |
| safeNativeSetVisible(false); |
| } else if (tab.getWebContents() == mWebContents) { |
| // The original tab was re-selected. Show it again and force an |
| // expansion on the bottom sheet. |
| if (!hint) { |
| // Here and below, we're only interested in restoring the UI for the |
| // case where hint is false, meaning that the tab is shown. This is |
| // the only way to be sure that the bottomsheet is unsuppressed when |
| // we try to restore the status to what it was prior to switching. |
| safeOnTabSelected(); |
| } |
| } else { |
| // |
| safeOnTabSwitched(getModel().getBottomSheetState(), |
| /* activityChanged = */ false); |
| // A new tab was selected. If Autofill Assistant is running on it, |
| // attach the UI to that other instance, otherwise destroy the UI. |
| AutofillAssistantClient.fromWebContents(mWebContents) |
| .transferUiTo(tab.getWebContents()); |
| if (!hint) { |
| safeOnTabSelected(); |
| } |
| } |
| } |
| |
| @Override |
| public void onActivityAttachmentChanged( |
| Tab tab, @Nullable WindowAndroid window) { |
| if (mWebContents == null) return; |
| |
| if (window == null && tab.getWebContents() == mWebContents) { |
| if (!allowTabSwitching) { |
| safeNativeStop(DropOutReason.TAB_DETACHED); |
| return; |
| } |
| |
| safeOnTabSwitched( |
| getModel().getBottomSheetState(), /* activityChanged = */ true); |
| // If we have an open snackbar, execute the callback immediately. This |
| // may shut down the Autofill Assistant. |
| if (mSnackbarController != null) { |
| mSnackbarController.onDismissNoAction(/* actionData= */ null); |
| } |
| AutofillAssistantClient.fromWebContents(mWebContents).destroyUi(); |
| } |
| } |
| }; |
| } |
| |
| private void setWebContentObserver(Tab tab) { |
| getModel().addObserver(new PropertyObserver<PropertyKey>() { |
| @Override |
| public void onPropertyChanged( |
| PropertyObservable<PropertyKey> source, @Nullable PropertyKey propertyKey) { |
| if (AssistantModel.WEB_CONTENTS == propertyKey) { |
| getModel().removeObserver(this); |
| if (tab != null |
| && tab.getWebContents() |
| == getModel().get(AssistantModel.WEB_CONTENTS)) { |
| safeOnTabSelected(); |
| } |
| } |
| } |
| }); |
| } |
| |
| // Native => Java methods. |
| |
| // TODO(crbug.com/806868): Some of these functions still have a little bit of logic (e.g. make |
| // the progress bar pulse when hiding overlay). Maybe it would be better to forward all calls to |
| // AssistantCoordinator (that way this bridge would only have a reference to that one) which in |
| // turn will forward calls to the other sub coordinators. The main reason this is not done yet |
| // is to avoid boilerplate. |
| |
| @CalledByNative |
| private void setWebContents(@Nullable WebContents webContents) { |
| mWebContents = webContents; |
| } |
| |
| @CalledByNative |
| private AssistantModel getModel() { |
| return mCoordinator.getModel(); |
| } |
| |
| @CalledByNative |
| private void clearNativePtr() { |
| mNativeUiController = 0; |
| mActivityTabObserver.destroy(); |
| mCoordinator.destroy(); |
| sActiveChromeActivities.remove(mActivity); |
| } |
| |
| /** |
| * Close CCT after the current task has finished running - usually after Autofill Assistant has |
| * finished shutting itself down. |
| */ |
| @CalledByNative |
| private void scheduleCloseCustomTab() { |
| if (mActivity instanceof CustomTabActivity) { |
| PostTask.postTask(UiThreadTaskTraits.DEFAULT, mActivity::finish); |
| } |
| } |
| |
| @CalledByNative |
| private void showContentAndExpandBottomSheet() { |
| mCoordinator.getBottomBarCoordinator().showContent( |
| /* shouldExpand = */ true, /* animate = */ true); |
| } |
| |
| @CalledByNative |
| private void expandBottomSheet() { |
| mCoordinator.getBottomBarCoordinator().expand(); |
| } |
| |
| @CalledByNative |
| private void collapseBottomSheet() { |
| mCoordinator.getBottomBarCoordinator().collapse(); |
| } |
| |
| @CalledByNative |
| private void showFeedback(String debugContext) { |
| mCoordinator.showFeedback(debugContext); |
| } |
| |
| @CalledByNative |
| private boolean isKeyboardShown() { |
| return mCoordinator.getKeyboardCoordinator().isKeyboardShown(); |
| } |
| |
| @CalledByNative |
| private void hideKeyboard() { |
| mCoordinator.getKeyboardCoordinator().hideKeyboard(); |
| } |
| |
| @CalledByNative |
| private void restoreBottomSheetState(@SheetState int state) { |
| mCoordinator.getBottomBarCoordinator().restoreState(state); |
| } |
| |
| @CalledByNative |
| private void hideKeyboardIfFocusNotOnText() { |
| mCoordinator.getKeyboardCoordinator().hideKeyboardIfFocusNotOnText(); |
| } |
| |
| @CalledByNative |
| private void showSnackbar(int delayMs, String message) { |
| mSnackbarController = AssistantSnackbar.show(mActivity, mActivity.getSnackbarManager(), |
| delayMs, message, this::safeSnackbarResult); |
| } |
| |
| private void dismissSnackbar() { |
| if (mSnackbarController != null) { |
| mActivity.getSnackbarManager().dismissSnackbars(mSnackbarController); |
| mSnackbarController = null; |
| } |
| } |
| |
| /** Creates an empty list of chips. */ |
| @CalledByNative |
| private static List<AssistantChip> createChipList() { |
| return new ArrayList<>(); |
| } |
| |
| /** |
| * Creates an action button which executes the action {@code actionIndex}. |
| */ |
| @CalledByNative |
| private AssistantChip createActionButton(int icon, String text, int actionIndex, |
| boolean disabled, boolean sticky, boolean visible, |
| @Nullable String contentDescription) { |
| AssistantChip chip = AssistantChip.createHairlineAssistantChip( |
| icon, text, disabled, sticky, visible, contentDescription); |
| chip.setSelectedListener(() -> safeNativeOnUserActionSelected(actionIndex)); |
| return chip; |
| } |
| |
| /** |
| * Creates a highlighted action button which executes the action {@code actionIndex}. |
| */ |
| @CalledByNative |
| private AssistantChip createHighlightedActionButton(int icon, String text, int actionIndex, |
| boolean disabled, boolean sticky, boolean visible, |
| @Nullable String contentDescription) { |
| AssistantChip chip = AssistantChip.createHighlightedAssistantChip( |
| icon, text, disabled, sticky, visible, contentDescription); |
| chip.setSelectedListener(() -> safeNativeOnUserActionSelected(actionIndex)); |
| return chip; |
| } |
| |
| /** |
| * Creates a cancel action button. If the keyboard is currently shown, it dismisses the |
| * keyboard. Otherwise, it shows the snackbar and then executes {@code actionIndex}, or shuts |
| * down Autofill Assistant if {@code actionIndex} is {@code -1}. |
| */ |
| @CalledByNative |
| private AssistantChip createCancelButton(int icon, String text, int actionIndex, |
| boolean disabled, boolean sticky, boolean visible, |
| @Nullable String contentDescription) { |
| AssistantChip chip = AssistantChip.createHairlineAssistantChip( |
| icon, text, disabled, sticky, visible, contentDescription); |
| chip.setSelectedListener(() -> safeNativeOnCancelButtonClicked(actionIndex)); |
| return chip; |
| } |
| |
| /** |
| * Adds a close action button to the chip list, which shuts down Autofill Assistant. |
| */ |
| @CalledByNative |
| private AssistantChip createCloseButton(int icon, String text, boolean disabled, boolean sticky, |
| boolean visible, @Nullable String contentDescription) { |
| AssistantChip chip = AssistantChip.createHairlineAssistantChip( |
| icon, text, disabled, sticky, visible, contentDescription); |
| chip.setSelectedListener(() -> safeNativeOnCloseButtonClicked()); |
| return chip; |
| } |
| |
| // TODO(arbesser): Remove this and use methods in {@code AssistantChip} instead. |
| @CalledByNative |
| private static void appendChipToList(List<AssistantChip> chips, AssistantChip chip) { |
| chips.add(chip); |
| } |
| |
| @CalledByNative |
| private void setActions(List<AssistantChip> chips) { |
| // TODO(b/144075373): Move this to AssistantCarouselModel. |
| getModel().getActionsModel().setChips(chips); |
| } |
| |
| @CalledByNative |
| private void setDisableChipChangeAnimations(boolean disable) { |
| // TODO(b/144075373): Move this to AssistantCarouselModel. |
| getModel().getActionsModel().setDisableChangeAnimations(disable); |
| } |
| |
| @CalledByNative |
| private void setViewportMode(@AssistantViewportMode int mode) { |
| mCoordinator.getBottomBarCoordinator().setViewportMode(mode); |
| } |
| |
| @CalledByNative |
| private void setPeekMode(@AssistantPeekHeightCoordinator.PeekMode int peekMode) { |
| mCoordinator.getBottomBarCoordinator().setPeekMode(peekMode); |
| } |
| |
| @CalledByNative |
| private Context getContext() { |
| return mActivity; |
| } |
| |
| // Native methods. |
| private void safeSnackbarResult(boolean undo) { |
| if (mSnackbarController != null && mNativeUiController != 0) { |
| AutofillAssistantUiControllerJni.get().snackbarResult( |
| mNativeUiController, AutofillAssistantUiController.this, undo); |
| mSnackbarController = null; |
| } |
| } |
| |
| private void safeNativeStop(@DropOutReason int reason) { |
| if (mNativeUiController != 0) { |
| AutofillAssistantUiControllerJni.get().stop( |
| mNativeUiController, AutofillAssistantUiController.this, reason); |
| } |
| } |
| |
| private void safeNativeOnFatalError(String message, @DropOutReason int reason) { |
| if (mNativeUiController != 0) { |
| AutofillAssistantUiControllerJni.get().onFatalError( |
| mNativeUiController, AutofillAssistantUiController.this, message, reason); |
| } |
| } |
| |
| private void safeNativeOnUserActionSelected(int index) { |
| if (mNativeUiController != 0) { |
| AutofillAssistantUiControllerJni.get().onUserActionSelected( |
| mNativeUiController, AutofillAssistantUiController.this, index); |
| } |
| } |
| |
| private void safeNativeOnCancelButtonClicked(int index) { |
| if (mNativeUiController != 0) { |
| AutofillAssistantUiControllerJni.get().onCancelButtonClicked( |
| mNativeUiController, AutofillAssistantUiController.this, index); |
| } |
| } |
| |
| private void safeNativeOnCloseButtonClicked() { |
| if (mNativeUiController != 0) { |
| AutofillAssistantUiControllerJni.get().onCloseButtonClicked( |
| mNativeUiController, AutofillAssistantUiController.this); |
| } |
| } |
| |
| private void safeNativeOnKeyboardVisibilityChanged(boolean visible) { |
| if (mNativeUiController != 0) { |
| AutofillAssistantUiControllerJni.get().onKeyboardVisibilityChanged( |
| mNativeUiController, AutofillAssistantUiController.this, visible); |
| } |
| } |
| |
| private void safeNativeSetVisible(boolean visible) { |
| if (mNativeUiController != 0) { |
| AutofillAssistantUiControllerJni.get().setVisible( |
| mNativeUiController, AutofillAssistantUiController.this, visible); |
| } |
| } |
| |
| private void safeOnTabSwitched(@SheetState int state, boolean activityChanged) { |
| if (mNativeUiController != 0) { |
| AutofillAssistantUiControllerJni.get().onTabSwitched(mNativeUiController, |
| AutofillAssistantUiController.this, state, activityChanged); |
| } |
| } |
| |
| private void safeOnTabSelected() { |
| if (mNativeUiController != 0) { |
| AutofillAssistantUiControllerJni.get().onTabSelected( |
| mNativeUiController, AutofillAssistantUiController.this); |
| } |
| } |
| |
| @NativeMethods |
| interface Natives { |
| void snackbarResult( |
| long nativeUiControllerAndroid, AutofillAssistantUiController caller, boolean undo); |
| void stop(long nativeUiControllerAndroid, AutofillAssistantUiController caller, |
| @DropOutReason int reason); |
| void onFatalError(long nativeUiControllerAndroid, AutofillAssistantUiController caller, |
| String message, @DropOutReason int reason); |
| void onUserActionSelected( |
| long nativeUiControllerAndroid, AutofillAssistantUiController caller, int index); |
| void onCancelButtonClicked( |
| long nativeUiControllerAndroid, AutofillAssistantUiController caller, int index); |
| void onCloseButtonClicked( |
| long nativeUiControllerAndroid, AutofillAssistantUiController caller); |
| void onKeyboardVisibilityChanged(long nativeUiControllerAndroid, |
| AutofillAssistantUiController caller, boolean visible); |
| void setVisible(long nativeUiControllerAndroid, AutofillAssistantUiController caller, |
| boolean visible); |
| void onTabSwitched(long nativeUiControllerAndroid, AutofillAssistantUiController caller, |
| @SheetState int state, boolean activityChanged); |
| void onTabSelected(long nativeUiControllerAndroid, AutofillAssistantUiController caller); |
| } |
| } |