1 // Copyright 2019 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.autofill_assistant; 6 7 import android.content.Context; 8 import android.os.Bundle; 9 10 import androidx.annotation.Nullable; 11 12 import org.chromium.base.Callback; 13 import org.chromium.base.ThreadUtils; 14 import org.chromium.chrome.browser.ActivityTabProvider; 15 import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider; 16 import org.chromium.chrome.browser.compositor.CompositorViewHolder; 17 import org.chromium.chrome.browser.directactions.DirectActionHandler; 18 import org.chromium.chrome.browser.directactions.DirectActionReporter; 19 import org.chromium.chrome.browser.directactions.DirectActionReporter.Definition; 20 import org.chromium.chrome.browser.directactions.DirectActionReporter.Type; 21 import org.chromium.chrome.browser.tab.Tab; 22 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController; 23 24 /** 25 * A handler that provides just enough functionality to allow on-demand loading of the module 26 * through direct actions. The actual implementation is in the module. 27 */ 28 public class AutofillAssistantDirectActionHandler implements DirectActionHandler { 29 private static final String FETCH_WEBSITE_ACTIONS = "fetch_website_actions"; 30 private static final String FETCH_WEBSITE_ACTIONS_RESULT = "success"; 31 private static final String AA_ACTION_RESULT = "success"; 32 private static final String ACTION_NAME = "name"; 33 private static final String EXPERIMENT_IDS = "experiment_ids"; 34 private static final String ONBOARDING_ACTION = "onboarding"; 35 private static final String USER_NAME = "user_name"; 36 37 private final Context mContext; 38 private final BottomSheetController mBottomSheetController; 39 private final BrowserControlsStateProvider mBrowserControls; 40 private final CompositorViewHolder mCompositorViewHolder; 41 private final ActivityTabProvider mActivityTabProvider; 42 private final AutofillAssistantModuleEntryProvider mModuleEntryProvider; 43 44 @Nullable 45 private AutofillAssistantActionHandler mDelegate; 46 AutofillAssistantDirectActionHandler(Context context, BottomSheetController bottomSheetController, BrowserControlsStateProvider browserControls, CompositorViewHolder compositorViewHolder, ActivityTabProvider activityTabProvider, AutofillAssistantModuleEntryProvider moduleEntryProvider)47 AutofillAssistantDirectActionHandler(Context context, 48 BottomSheetController bottomSheetController, 49 BrowserControlsStateProvider browserControls, CompositorViewHolder compositorViewHolder, 50 ActivityTabProvider activityTabProvider, 51 AutofillAssistantModuleEntryProvider moduleEntryProvider) { 52 mContext = context; 53 mBottomSheetController = bottomSheetController; 54 mBrowserControls = browserControls; 55 mCompositorViewHolder = compositorViewHolder; 56 mActivityTabProvider = activityTabProvider; 57 mModuleEntryProvider = moduleEntryProvider; 58 } 59 60 @Override reportAvailableDirectActions(DirectActionReporter reporter)61 public void reportAvailableDirectActions(DirectActionReporter reporter) { 62 if (!AutofillAssistantPreferencesUtil.isAutofillAssistantSwitchOn()) { 63 return; 64 } 65 66 if (!AutofillAssistantPreferencesUtil.isAutofillOnboardingAccepted()) { 67 reporter.addDirectAction(ONBOARDING_ACTION) 68 .withParameter(ACTION_NAME, Type.STRING, /* required= */ false) 69 .withParameter(EXPERIMENT_IDS, Type.STRING, /* required= */ false) 70 .withResult(AA_ACTION_RESULT, Type.BOOLEAN); 71 return; 72 } 73 74 ThreadUtils.assertOnUiThread(); 75 if (mDelegate == null || (mDelegate != null && !mDelegate.hasRunFirstCheck())) { 76 reporter.addDirectAction(FETCH_WEBSITE_ACTIONS) 77 .withParameter(USER_NAME, Type.STRING, /* required= */ false) 78 .withParameter(EXPERIMENT_IDS, Type.STRING, /* required= */ false) 79 .withResult(FETCH_WEBSITE_ACTIONS_RESULT, Type.BOOLEAN); 80 } else { 81 // Otherwise we are already done fetching scripts and can just return the ones we know 82 // about. 83 for (AutofillAssistantDirectAction action : mDelegate.getActions()) { 84 for (String name : action.getNames()) { 85 Definition definition = reporter.addDirectAction(name) 86 .withParameter(EXPERIMENT_IDS, Type.STRING, 87 /* required= */ false) 88 .withResult(AA_ACTION_RESULT, Type.BOOLEAN); 89 90 // TODO(b/138833619): Support non-string arguments. Requires updating the proto 91 // definition. 92 for (String required : action.getRequiredArguments()) { 93 definition.withParameter(required, Type.STRING, /* required= */ true); 94 } 95 for (String optional : action.getOptionalArguments()) { 96 definition.withParameter(optional, Type.STRING, /* required= */ false); 97 } 98 } 99 } 100 } 101 } 102 103 @Override performDirectAction( String actionId, Bundle arguments, Callback<Bundle> callback)104 public boolean performDirectAction( 105 String actionId, Bundle arguments, Callback<Bundle> callback) { 106 if (actionId.equals(FETCH_WEBSITE_ACTIONS) 107 && AutofillAssistantPreferencesUtil.isAutofillOnboardingAccepted()) { 108 fetchWebsiteActions(arguments, callback); 109 return true; 110 } 111 // Only handle and perform the action if it is known to the controller. 112 if (isActionAvailable(actionId) || ONBOARDING_ACTION.equals(actionId)) { 113 performAction(actionId, arguments, callback); 114 return true; 115 } 116 return false; 117 } 118 isActionAvailable(String actionId)119 private boolean isActionAvailable(String actionId) { 120 if (mDelegate == null) return false; 121 for (AutofillAssistantDirectAction action : mDelegate.getActions()) { 122 if (action.getNames().contains(actionId)) return true; 123 } 124 return false; 125 } 126 fetchWebsiteActions(Bundle arguments, Callback<Bundle> bundleCallback)127 private void fetchWebsiteActions(Bundle arguments, Callback<Bundle> bundleCallback) { 128 Callback<Boolean> successCallback = (success) -> { 129 Bundle bundle = new Bundle(); 130 bundle.putBoolean(FETCH_WEBSITE_ACTIONS_RESULT, success); 131 bundleCallback.onResult(bundle); 132 }; 133 134 if (!AutofillAssistantPreferencesUtil.isAutofillAssistantSwitchOn()) { 135 successCallback.onResult(false); 136 return; 137 } 138 139 if (!AutofillAssistantPreferencesUtil.isAutofillOnboardingAccepted()) { 140 successCallback.onResult(false); 141 return; 142 } 143 144 String userName = arguments.getString(USER_NAME, ""); 145 arguments.remove(USER_NAME); 146 147 String experimentIds = arguments.getString(EXPERIMENT_IDS, ""); 148 arguments.remove(EXPERIMENT_IDS); 149 150 getDelegate(/* installIfNecessary= */ false, (delegate) -> { 151 if (delegate == null) { 152 successCallback.onResult(false); 153 return; 154 } 155 delegate.fetchWebsiteActions(userName, experimentIds, arguments, successCallback); 156 }); 157 } 158 performAction(String actionId, Bundle arguments, Callback<Bundle> bundleCallback)159 private void performAction(String actionId, Bundle arguments, Callback<Bundle> bundleCallback) { 160 Callback<Boolean> booleanCallback = (result) -> { 161 Bundle bundle = new Bundle(); 162 bundle.putBoolean(AA_ACTION_RESULT, result); 163 bundleCallback.onResult(bundle); 164 }; 165 166 if (!AutofillAssistantPreferencesUtil.isAutofillAssistantSwitchOn()) { 167 booleanCallback.onResult(false); 168 return; 169 } 170 171 String experimentIds = arguments.getString(EXPERIMENT_IDS, ""); 172 arguments.remove(EXPERIMENT_IDS); 173 174 getDelegate(/* installIfNecessary= */ true, (delegate) -> { 175 if (delegate == null) { 176 booleanCallback.onResult(false); 177 return; 178 } 179 if (ONBOARDING_ACTION.equals(actionId)) { 180 delegate.performOnboarding(experimentIds, arguments, booleanCallback); 181 return; 182 } 183 184 Callback<Boolean> successCallback = (success) -> { 185 booleanCallback.onResult(success && !delegate.getActions().isEmpty()); 186 }; 187 delegate.performAction(actionId, experimentIds, arguments, successCallback); 188 }); 189 } 190 191 /** 192 * Builds the delegate, if possible, and pass it to the callback. 193 * 194 * <p>If necessary, this function creates a delegate instance and keeps it in {@link 195 * #mDelegate}. 196 * 197 * @param installIfNecessary if true, install the DFM if necessary 198 * @param callback callback to report the delegate to 199 */ getDelegate( boolean installIfNecessary, Callback<AutofillAssistantActionHandler> callback)200 private void getDelegate( 201 boolean installIfNecessary, Callback<AutofillAssistantActionHandler> callback) { 202 if (mDelegate == null) { 203 mDelegate = createDelegate(mModuleEntryProvider.getModuleEntryIfInstalled()); 204 } 205 if (mDelegate != null || !installIfNecessary) { 206 callback.onResult(mDelegate); 207 return; 208 } 209 210 Tab tab = mActivityTabProvider.get(); 211 if (tab == null) { 212 // TODO(b/134741524): Allow DFM loading UI to work with no tabs. 213 callback.onResult(null); 214 return; 215 } 216 mModuleEntryProvider.getModuleEntry(tab, (entry) -> { 217 mDelegate = createDelegate(entry); 218 callback.onResult(mDelegate); 219 }, /* showUi = */ true); 220 } 221 222 /** Creates a delegate from the given {@link AutofillAssistantModuleEntry}, if possible. */ 223 @Nullable createDelegate( @ullable AutofillAssistantModuleEntry entry)224 private AutofillAssistantActionHandler createDelegate( 225 @Nullable AutofillAssistantModuleEntry entry) { 226 if (entry == null) return null; 227 228 return entry.createActionHandler(mContext, mBottomSheetController, mBrowserControls, 229 mCompositorViewHolder, mActivityTabProvider); 230 } 231 } 232