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