Commit 7ddd62bf authored by Clemens Arbesser's avatar Clemens Arbesser Committed by Commit Bot

[Autofill Assistant] Added UI for trigger scripts.

This adds the UI only. This is not yet functional, because the link to
native is not yet implemented. I plan to submit the necessary native CLs
first and then do the required JNI plumbing to stitch everything
together.

Keeping things separate makes for easier developing and (hopefully)
reviewing. See screenshot of the UI in the attached bug.

Bug: b/171776026
Change-Id: Ic7da1904a6a3c33d19c780c7dd5b9450599ba443
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2510053
Commit-Queue: Clemens Arbesser <arbesser@google.com>
Reviewed-by: default avatarSandro Maggi <sandromaggi@google.com>
Reviewed-by: default avatarMarian Fechete <marianfe@google.com>
Cr-Commit-Position: refs/heads/master@{#823585}
parent 05d645f9
......@@ -159,6 +159,7 @@ android_library("java") {
"java/src/org/chromium/chrome/browser/autofill_assistant/overlay/AssistantOverlayEventFilter.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/overlay/AssistantOverlayImage.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/overlay/AssistantOverlayModel.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/trigger_scripts/AssistantTriggerScript.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/user_data/AssistantChoiceList.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/user_data/AssistantCollectUserDataBinder.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/user_data/AssistantCollectUserDataCoordinator.java",
......@@ -283,6 +284,7 @@ android_library("test_java") {
"javatests/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantProgressBarIntegrationTest.java",
"javatests/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantPromptNavigationIntegrationTest.java",
"javatests/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantTextUtilsTest.java",
"javatests/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantTriggerScriptTest.java",
"javatests/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantUiTest.java",
"javatests/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantUiTestUtil.java",
"javatests/src/org/chromium/chrome/browser/autofill_assistant/TestingAutofillAssistantModuleEntryProvider.java",
......
......@@ -21,7 +21,7 @@ import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
* BottomSheet when its size changes and allows to dynamically set its scrollable content (in
* practice, this allows to replace the onboarding by the actual Autofill Assistant content).
*/
class AssistantBottomSheetContent implements BottomSheetContent {
public class AssistantBottomSheetContent implements BottomSheetContent {
private final View mToolbarView;
private final SizeListenableLinearLayout mContentView;
@Nullable
......@@ -50,7 +50,7 @@ class AssistantBottomSheetContent implements BottomSheetContent {
mContentScrollableView = scrollableView;
}
void setPeekModeDisabled(boolean disabled) {
public void setPeekModeDisabled(boolean disabled) {
mPeekModeDisabled = disabled;
}
......
......@@ -11,9 +11,13 @@ import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.Shee
import org.chromium.components.browser_ui.bottomsheet.EmptyBottomSheetObserver;
import org.chromium.content_public.browser.UiThreadTaskTraits;
class BottomSheetUtils {
/**
* Utility class to facilitate showing instances of {@code AssistantBottomSheetContent} in the
* Chrome bottom sheet.
*/
public class BottomSheetUtils {
/** Request {@code controller} to show {@code content} and expand the sheet when it is shown. */
static void showContentAndMaybeExpand(BottomSheetController controller,
public static void showContentAndMaybeExpand(BottomSheetController controller,
AssistantBottomSheetContent content, boolean shouldExpand, boolean animate) {
// Show the content.
boolean contentShown = controller.requestShowContent(content, animate);
......
// Copyright 2020 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.trigger_scripts;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.Space;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.chrome.autofill_assistant.R;
import org.chromium.chrome.browser.autofill_assistant.AssistantBottomBarDelegate;
import org.chromium.chrome.browser.autofill_assistant.AssistantBottomSheetContent;
import org.chromium.chrome.browser.autofill_assistant.AssistantRootViewContainer;
import org.chromium.chrome.browser.autofill_assistant.BottomSheetUtils;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantChip;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantChipViewHolder;
import org.chromium.chrome.browser.autofill_assistant.generic_ui.AssistantDimension;
import org.chromium.chrome.browser.autofill_assistant.header.AssistantHeaderCoordinator;
import org.chromium.chrome.browser.autofill_assistant.header.AssistantHeaderModel;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.StateChangeReason;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetObserver;
import org.chromium.components.browser_ui.bottomsheet.EmptyBottomSheetObserver;
import java.util.ArrayList;
import java.util.List;
/**
* Represents the UI for a particular trigger script. Notifies a delegate about user interactions.
*/
public class AssistantTriggerScript {
/** Interface for delegates of this class who will be notified of user interactions. */
public interface Delegate {
void onTriggerScriptAction(@TriggerScriptAction int action);
void onBottomSheetClosedWithSwipe();
void onBackButtonPressed();
void onFeedbackButtonClicked();
}
@Nullable
private final AssistantBottomSheetContent mContent;
private final Context mContext;
private final Delegate mDelegate;
private final BottomSheetController mBottomSheetController;
private final BottomSheetObserver mBottomSheetObserver;
private final AssistantRootViewContainer mRootViewContainer;
private final ScrollView mScrollableContent;
private final AssistantHeaderCoordinator mHeaderCoordinator;
private final AssistantHeaderModel mHeaderModel;
private final LinearLayout mChipsContainer;
private final int mInnerChipSpacing;
private final List<AssistantChip> mLeftAlignedChips = new ArrayList<>();
private final List<AssistantChip> mRightAlignedChips = new ArrayList<>();
private boolean mAnimateBottomSheet = true;
public AssistantTriggerScript(
Context context, Delegate delegate, BottomSheetController controller) {
assert delegate != null;
mContext = context;
mDelegate = delegate;
mBottomSheetController = controller;
mBottomSheetObserver = new EmptyBottomSheetObserver() {
@Override
public void onSheetClosed(@StateChangeReason int reason) {
if (reason == StateChangeReason.SWIPE) {
mDelegate.onBottomSheetClosedWithSwipe();
}
}
};
mContent =
new AssistantBottomSheetContent(mContext, () -> new AssistantBottomBarDelegate() {
@Override
public boolean onBackButtonPressed() {
// We need to handle this event, because by default the bottom sheet will
// close when the back button is pressed.
mDelegate.onBackButtonPressed();
return true;
}
@Override
public void onBottomSheetClosedWithSwipe() {
// TODO(micantox): introduce a dedicated AssistantBottomSheetContent
// delegate, rather than reuse the bottom bar delegate, as it forces
// implementation of methods that the bottom sheet will never invoke.
assert false : "This should never happen";
mDelegate.onBottomSheetClosedWithSwipe();
}
});
// Allow swipe-to-dismiss.
mContent.setPeekModeDisabled(true);
mHeaderModel = new AssistantHeaderModel();
mHeaderModel.set(
AssistantHeaderModel.FEEDBACK_BUTTON_CALLBACK, mDelegate::onFeedbackButtonClicked);
mHeaderCoordinator = new AssistantHeaderCoordinator(context, mHeaderModel);
mChipsContainer = new LinearLayout(context);
mChipsContainer.setOrientation(LinearLayout.HORIZONTAL);
int horizontalMargin = context.getResources().getDimensionPixelSize(
R.dimen.autofill_assistant_bottombar_horizontal_spacing);
int verticalMargin = AssistantDimension.getPixelSizeDp(context, 16);
mChipsContainer.setPadding(
horizontalMargin, verticalMargin, horizontalMargin, verticalMargin);
mInnerChipSpacing = mContext.getResources().getDimensionPixelSize(
R.dimen.autofill_assistant_actions_spacing);
mRootViewContainer = (AssistantRootViewContainer) LayoutInflater.from(context).inflate(
R.layout.autofill_assistant_bottom_sheet_content, /* root= */ null);
mScrollableContent = mRootViewContainer.findViewById(R.id.scrollable_content);
mRootViewContainer.addView(mHeaderCoordinator.getView(), 0);
mRootViewContainer.addView(mChipsContainer,
new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
mContent.setContent(mRootViewContainer, mScrollableContent);
}
public void destroy() {
mBottomSheetController.removeObserver(mBottomSheetObserver);
mHeaderCoordinator.destroy();
}
@VisibleForTesting
public void disableBottomSheetAnimationsForTesting(boolean disable) {
mAnimateBottomSheet = !disable;
}
@VisibleForTesting
public AssistantHeaderModel getHeaderModelForTest() {
return mHeaderModel;
}
@VisibleForTesting
public List<AssistantChip> getLeftAlignedChipsForTest() {
return mLeftAlignedChips;
}
@VisibleForTesting
public List<AssistantChip> getRightAlignedChipsForTest() {
return mRightAlignedChips;
}
private void addChipsToContainer(LinearLayout container, List<AssistantChip> chips) {
for (int i = 0; i < chips.size(); ++i) {
AssistantChipViewHolder viewHolder =
AssistantChipViewHolder.create(container, chips.get(i).getType());
viewHolder.bind(chips.get(i));
container.addView(viewHolder.getView());
if (i < chips.size() - 1) {
container.addView(new Space(mContext),
new LinearLayout.LayoutParams(
mInnerChipSpacing, ViewGroup.LayoutParams.MATCH_PARENT));
}
}
}
// TODO(b/171776026): before calling this method, native needs to send the necessary
// information to populate and update the views.
public void show() {
mChipsContainer.removeAllViews();
addChipsToContainer(mChipsContainer, mLeftAlignedChips);
mChipsContainer.addView(new Space(mContext),
new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f));
addChipsToContainer(mChipsContainer, mRightAlignedChips);
mBottomSheetController.removeObserver(mBottomSheetObserver);
mBottomSheetController.addObserver(mBottomSheetObserver);
BottomSheetUtils.showContentAndMaybeExpand(mBottomSheetController, mContent,
/* shouldExpand = */ true, /* animate = */ mAnimateBottomSheet);
}
public void hide() {
mBottomSheetController.removeObserver(mBottomSheetObserver);
mBottomSheetController.hideContent(mContent, /* animate = */ mAnimateBottomSheet);
}
}
// Copyright 2020 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 static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import android.support.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.autofill_assistant.R;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantChip;
import org.chromium.chrome.browser.autofill_assistant.drawable.AssistantDrawableIcon;
import org.chromium.chrome.browser.autofill_assistant.generic_ui.AssistantDrawable;
import org.chromium.chrome.browser.autofill_assistant.header.AssistantHeaderModel;
import org.chromium.chrome.browser.autofill_assistant.trigger_scripts.AssistantTriggerScript;
import org.chromium.chrome.browser.customtabs.CustomTabActivityTestRule;
import org.chromium.chrome.browser.customtabs.CustomTabsTestUtils;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import java.util.Arrays;
import java.util.List;
/** UI tests for {@code AssistantTriggerScript}. */
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@RunWith(ChromeJUnit4ClassRunner.class)
public class AutofillAssistantTriggerScriptTest {
private static final String TEST_PAGE = "/components/test/data/autofill_assistant/html/"
+ "autofill_assistant_target_website.html";
@Rule
public CustomTabActivityTestRule mTestRule = new CustomTabActivityTestRule();
@Before
public void setUp() {
mTestRule.startCustomTabActivityWithIntent(CustomTabsTestUtils.createMinimalCustomTabIntent(
InstrumentationRegistry.getTargetContext(),
mTestRule.getTestServer().getURL(TEST_PAGE)));
}
protected BottomSheetController getBottomSheetController() {
return AutofillAssistantUiTestUtil.getBottomSheetController(mTestRule.getActivity());
}
@Test
@MediumTest
public void testTriggerScript() throws Exception {
AssistantTriggerScript triggerScript = TestThreadUtils.runOnUiThreadBlocking(
()
-> new AssistantTriggerScript(
mTestRule.getActivity(), new AssistantTriggerScript.Delegate() {
@Override
public void onTriggerScriptAction(int action) {}
@Override
public void onBottomSheetClosedWithSwipe() {}
@Override
public void onBackButtonPressed() {}
@Override
public void onFeedbackButtonClicked() {}
}, getBottomSheetController()));
TestThreadUtils.runOnUiThreadBlocking(() -> {
AssistantHeaderModel headerModel = triggerScript.getHeaderModelForTest();
headerModel.set(AssistantHeaderModel.STATUS_MESSAGE, "Hello world!");
headerModel.set(AssistantHeaderModel.USE_STEP_PROGRESS_BAR, true);
headerModel.set(AssistantHeaderModel.STEP_PROGRESS_BAR_ICONS,
Arrays.asList(AssistantDrawable.createFromIcon(
AssistantDrawableIcon.PROGRESSBAR_DEFAULT_INITIAL_STEP),
AssistantDrawable.createFromIcon(
AssistantDrawableIcon.PROGRESSBAR_DEFAULT_DATA_COLLECTION),
AssistantDrawable.createFromIcon(
AssistantDrawableIcon.PROGRESSBAR_DEFAULT_PAYMENT),
AssistantDrawable.createFromIcon(
AssistantDrawableIcon.PROGRESSBAR_DEFAULT_FINAL_STEP)));
headerModel.set(AssistantHeaderModel.PROGRESS_ACTIVE_STEP, 1);
List<AssistantChip> leftAlignedChips = triggerScript.getLeftAlignedChipsForTest();
leftAlignedChips.add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE,
AssistantChip.Icon.OVERFLOW, "", false, false, true, () -> {}));
List<AssistantChip> rightAlignedChips = triggerScript.getRightAlignedChipsForTest();
rightAlignedChips.add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE,
AssistantChip.Icon.NONE, "Not now", false, false, true, () -> {}));
rightAlignedChips.add(new AssistantChip(AssistantChip.Type.BUTTON_FILLED_BLUE,
AssistantChip.Icon.NONE, "Fast checkout", false, false, true, () -> {}));
});
triggerScript.disableBottomSheetAnimationsForTesting(true);
TestThreadUtils.runOnUiThreadBlocking(triggerScript::show);
onView(withId(R.id.autofill_assistant)).check(matches(isDisplayed()));
onView(withId(R.id.header)).check(matches(isDisplayed()));
onView(withId(R.id.poodle_wrapper)).check(matches(isDisplayed()));
onView(withText("Hello world!")).check(matches(isDisplayed()));
onView(withId(R.id.profile_image)).check(matches(isDisplayed()));
onView(withId(R.id.step_progress_bar)).check(matches(isDisplayed()));
onView(withContentDescription(R.string.autofill_assistant_overflow_options))
.check(matches(isDisplayed()));
onView(withText("Not now")).check(matches(isDisplayed()));
onView(withText("Fast checkout")).check(matches(isDisplayed()));
TestThreadUtils.runOnUiThreadBlocking(triggerScript::hide);
onView(withId(R.id.autofill_assistant)).check(doesNotExist());
TestThreadUtils.runOnUiThreadBlocking(triggerScript::destroy);
}
}
......@@ -407,6 +407,7 @@ if (is_android) {
"generic_ui_java_generated_enums.h",
"metrics.h",
"overlay_state.h",
"trigger_scripts/trigger_script_action.h",
"user_data.h",
"viewport_mode.h",
]
......
// Copyright 2020 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.
#ifndef COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_TRIGGER_SCRIPTS_TRIGGER_SCRIPT_ACTION_H_
#define COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_TRIGGER_SCRIPTS_TRIGGER_SCRIPT_ACTION_H_
namespace autofill_assistant {
// C++ enum corresponding to the TriggerScriptProto::TriggerScriptAction enum
// defined in proto.
//
// GENERATED_JAVA_ENUM_PACKAGE: (
// org.chromium.chrome.browser.autofill_assistant.trigger_scripts)
// GENERATED_JAVA_CLASS_NAME_OVERRIDE: TriggerScriptAction
enum class TriggerScriptAction {
UNDEFINED = 0,
NOT_NOW = 1,
CANCEL_SESSION = 2,
CANCEL_FOREVER = 3,
SHOW_CANCEL_POPUP = 4,
ACCEPT = 5
};
} // namespace autofill_assistant
#endif // COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_TRIGGER_SCRIPTS_TRIGGER_SCRIPT_ACTION_H_
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment