Commit c93b69b4 authored by Clemens Arbesser's avatar Clemens Arbesser Committed by Commit Bot

[Autofill Assistant] Fixed animation issues in action carousel.

Bug: b/144075373
Change-Id: I559fb7bd9ad4273e3360604c24c230ce5edc0ef9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2043799
Commit-Queue: Clemens Arbesser <arbesser@google.com>
Reviewed-by: default avatarSandro Maggi <sandromaggi@google.com>
Cr-Commit-Position: refs/heads/master@{#742170}
parent c048b6f5
...@@ -91,13 +91,13 @@ android_library("java") { ...@@ -91,13 +91,13 @@ android_library("java") {
"java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantServiceInjector.java", "java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantServiceInjector.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantUiController.java", "java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantUiController.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/BottomSheetUtils.java", "java/src/org/chromium/chrome/browser/autofill_assistant/BottomSheetUtils.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/EditDistance.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/FeedbackContext.java", "java/src/org/chromium/chrome/browser/autofill_assistant/FeedbackContext.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/SizeListenableLinearLayout.java", "java/src/org/chromium/chrome/browser/autofill_assistant/SizeListenableLinearLayout.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantActionsCarouselCoordinator.java", "java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantActionsCarouselCoordinator.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantActionsDecoration.java", "java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantActionsDecoration.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantCarouselModel.java", "java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantCarouselModel.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantChip.java", "java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantChip.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantChipAdapter.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantChipViewHolder.java", "java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantChipViewHolder.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/carousel/ButtonView.java", "java/src/org/chromium/chrome/browser/autofill_assistant/carousel/ButtonView.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/details/AssistantDetails.java", "java/src/org/chromium/chrome/browser/autofill_assistant/details/AssistantDetails.java",
...@@ -242,7 +242,6 @@ android_library("test_java") { ...@@ -242,7 +242,6 @@ android_library("test_java") {
"javatests/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantTextUtilsTest.java", "javatests/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantTextUtilsTest.java",
"javatests/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantUiTest.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/AutofillAssistantUiTestUtil.java",
"javatests/src/org/chromium/chrome/browser/autofill_assistant/EditDistanceTest.java",
"javatests/src/org/chromium/chrome/browser/autofill_assistant/TestingAutofillAssistantModuleEntryProvider.java", "javatests/src/org/chromium/chrome/browser/autofill_assistant/TestingAutofillAssistantModuleEntryProvider.java",
] ]
......
...@@ -19,7 +19,7 @@ import org.chromium.base.ObserverList; ...@@ -19,7 +19,7 @@ import org.chromium.base.ObserverList;
import org.chromium.chrome.autofill_assistant.R; import org.chromium.chrome.autofill_assistant.R;
import org.chromium.chrome.browser.ChromeActivity; import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantActionsCarouselCoordinator; import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantActionsCarouselCoordinator;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantChip; import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantCarouselModel;
import org.chromium.chrome.browser.autofill_assistant.details.AssistantDetailsCoordinator; import org.chromium.chrome.browser.autofill_assistant.details.AssistantDetailsCoordinator;
import org.chromium.chrome.browser.autofill_assistant.form.AssistantFormCoordinator; import org.chromium.chrome.browser.autofill_assistant.form.AssistantFormCoordinator;
import org.chromium.chrome.browser.autofill_assistant.form.AssistantFormModel; import org.chromium.chrome.browser.autofill_assistant.form.AssistantFormModel;
...@@ -35,7 +35,6 @@ import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetContent; ...@@ -35,7 +35,6 @@ import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetContent;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetController; import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetController;
import org.chromium.chrome.browser.widget.bottomsheet.EmptyBottomSheetObserver; import org.chromium.chrome.browser.widget.bottomsheet.EmptyBottomSheetObserver;
import org.chromium.content_public.browser.WebContents; import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.modelutil.ListModel;
/** /**
* Coordinator responsible for the Autofill Assistant bottom bar. * Coordinator responsible for the Autofill Assistant bottom bar.
...@@ -149,7 +148,7 @@ class AssistantBottomBarCoordinator ...@@ -149,7 +148,7 @@ class AssistantBottomBarCoordinator
setChildMarginTop(mFormCoordinator.getView(), childSpacing); setChildMarginTop(mFormCoordinator.getView(), childSpacing);
// Hide the carousels when they are empty. // Hide the carousels when they are empty.
hideWhenEmpty(mActionsCoordinator.getView(), model.getActionsModel().getChipsModel()); hideWhenEmpty(mActionsCoordinator.getView(), model.getActionsModel());
// Set the horizontal margins of children. We don't set them on the payment request, the // Set the horizontal margins of children. We don't set them on the payment request, the
// carousels or the form to allow them to take the full width of the sheet. // carousels or the form to allow them to take the full width of the sheet.
...@@ -325,18 +324,16 @@ class AssistantBottomBarCoordinator ...@@ -325,18 +324,16 @@ class AssistantBottomBarCoordinator
/** /**
* Observe {@code model} such that the associated view is made invisible when it is empty. * Observe {@code model} such that the associated view is made invisible when it is empty.
*/ */
private void hideWhenEmpty(View carouselView, ListModel<AssistantChip> chipsModel) { private void hideWhenEmpty(View carouselView, AssistantCarouselModel carouselModel) {
setCarouselVisibility(carouselView, chipsModel); setCarouselVisibility(carouselView, carouselModel);
chipsModel.addObserver(new AbstractListObserver<Void>() { carouselModel.addObserver(
@Override (source, propertyKey) -> setCarouselVisibility(carouselView, carouselModel));
public void onDataSetChanged() {
setCarouselVisibility(carouselView, chipsModel);
}
});
} }
private void setCarouselVisibility(View carouselView, ListModel<AssistantChip> chipsModel) { private void setCarouselVisibility(View carouselView, AssistantCarouselModel carouselModel) {
carouselView.setVisibility(chipsModel.size() > 0 ? View.VISIBLE : View.GONE); carouselView.setVisibility(carouselModel.get(AssistantCarouselModel.CHIPS).size() > 0
? View.VISIBLE
: View.GONE);
} }
private void setHorizontalMargins(View view) { private void setHorizontalMargins(View view) {
......
...@@ -14,6 +14,7 @@ import org.chromium.base.task.PostTask; ...@@ -14,6 +14,7 @@ import org.chromium.base.task.PostTask;
import org.chromium.chrome.autofill_assistant.R; import org.chromium.chrome.autofill_assistant.R;
import org.chromium.chrome.browser.ActivityTabProvider; import org.chromium.chrome.browser.ActivityTabProvider;
import org.chromium.chrome.browser.ChromeActivity; import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantCarouselModel;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantChip; import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantChip;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantChip.Type; import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantChip.Type;
import org.chromium.chrome.browser.autofill_assistant.header.AssistantHeaderModel; import org.chromium.chrome.browser.autofill_assistant.header.AssistantHeaderModel;
...@@ -296,15 +297,29 @@ class AutofillAssistantUiController { ...@@ -296,15 +297,29 @@ class AutofillAssistantUiController {
@CalledByNative @CalledByNative
private void setActions(List<AssistantChip> chips) { private void setActions(List<AssistantChip> chips) {
setActionChips(chips); // TODO(b/144075373): Move this to AssistantCarouselModel and AssistantHeaderModel. Move
// header chip logic to native.
AssistantCarouselModel model = getModel().getActionsModel();
model.setChips(chips);
setHeaderChip(chips); setHeaderChip(chips);
} }
@CalledByNative @CalledByNative
private void setAllChipsVisibleExcept(String identifier, boolean visible) { private void setAllChipsVisibleExcept(String identifier, boolean visible) {
mCoordinator.getBottomBarCoordinator() AssistantCarouselModel model = getModel().getActionsModel();
.getActionsCarouselCoordinator() List<AssistantChip> chips = model.get(AssistantCarouselModel.CHIPS);
.setAllChipsVisibleExcept(identifier, visible); // Copy the list and modify the copy. Modifying the actual list in-place will not fire the
// relevant change notifications. TODO(b/144075373): Refactor to avoid this deep copy,
// preferably by moving this to native.
List<AssistantChip> newChips = new ArrayList<>();
for (int i = 0; i < chips.size(); ++i) {
AssistantChip newChip = new AssistantChip(chips.get(i));
newChips.add(newChip);
if (!chips.get(i).getIdentifier().equals(identifier)) {
newChip.setVisible(visible);
}
}
model.setChips(newChips);
} }
private void setHeaderChip(List<AssistantChip> chips) { private void setHeaderChip(List<AssistantChip> chips) {
...@@ -320,12 +335,6 @@ class AutofillAssistantUiController { ...@@ -320,12 +335,6 @@ class AutofillAssistantUiController {
getModel().getHeaderModel().set(AssistantHeaderModel.CHIP, headerChip); getModel().getHeaderModel().set(AssistantHeaderModel.CHIP, headerChip);
} }
private void setActionChips(List<AssistantChip> newChips) {
// TODO(b/144075373): Move this to AssistantActionsCarouselCoordinator.
mCoordinator.getBottomBarCoordinator().getActionsCarouselCoordinator().updateChips(
newChips);
}
@CalledByNative @CalledByNative
private void setViewportMode(@AssistantViewportMode int mode) { private void setViewportMode(@AssistantViewportMode int mode) {
mCoordinator.getBottomBarCoordinator().setViewportMode(mode); mCoordinator.getBottomBarCoordinator().setViewportMode(mode);
......
...@@ -13,12 +13,6 @@ import android.view.View; ...@@ -13,12 +13,6 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.chromium.chrome.autofill_assistant.R; import org.chromium.chrome.autofill_assistant.R;
import org.chromium.chrome.browser.autofill_assistant.EditDistance;
import org.chromium.ui.modelutil.ListModel;
import org.chromium.ui.modelutil.RecyclerViewAdapter;
import org.chromium.ui.modelutil.SimpleRecyclerViewMcp;
import java.util.List;
/** /**
* A coordinator responsible for suggesting chips to the user. If there is one chip to display, it * A coordinator responsible for suggesting chips to the user. If there is one chip to display, it
...@@ -42,14 +36,13 @@ import java.util.List; ...@@ -42,14 +36,13 @@ import java.util.List;
* | | * | |
*/ */
public class AssistantActionsCarouselCoordinator { public class AssistantActionsCarouselCoordinator {
private final AssistantCarouselModel mModel;
private final RecyclerView mView; private final RecyclerView mView;
public AssistantActionsCarouselCoordinator(Context context, AssistantCarouselModel model) { public AssistantActionsCarouselCoordinator(Context context, AssistantCarouselModel model) {
mModel = model;
mView = new RecyclerView(context); mView = new RecyclerView(context);
mView.setTag(RECYCLER_VIEW_TAG); mView.setTag(RECYCLER_VIEW_TAG);
AssistantChipAdapter chipAdapter = new AssistantChipAdapter();
mView.setAdapter(chipAdapter);
CustomLayoutManager layoutManager = new CustomLayoutManager(); CustomLayoutManager layoutManager = new CustomLayoutManager();
// Workaround for b/128679161. // Workaround for b/128679161.
...@@ -70,77 +63,17 @@ public class AssistantActionsCarouselCoordinator { ...@@ -70,77 +63,17 @@ public class AssistantActionsCarouselCoordinator {
* context.getResources().getDimensionPixelSize( * context.getResources().getDimensionPixelSize(
R.dimen.autofill_assistant_button_bg_vertical_inset))); R.dimen.autofill_assistant_button_bg_vertical_inset)));
mView.setAdapter(new RecyclerViewAdapter<>( model.addObserver((source, propertyKey) -> {
new SimpleRecyclerViewMcp<>(model.getChipsModel(), if (propertyKey == AssistantCarouselModel.CHIPS) {
AssistantChipViewHolder::getViewType, AssistantChipViewHolder::bind), chipAdapter.setChips(model.get(AssistantCarouselModel.CHIPS));
AssistantChipViewHolder::create)); }
});
} }
public RecyclerView getView() { public RecyclerView getView() {
return mView; return mView;
} }
public void updateChips(List<AssistantChip> newChips) {
ListModel<AssistantChip> oldChips = mModel.getChipsModel();
if (!areChipsEqual(oldChips, newChips)) {
// We apply the minimum set of operations on the current chips to transform it in the
// target list of chips. When testing for chip equivalence, we only compare their type
// and text but all substitutions will still be applied so we are sure we display the
// given {@code chips} with their associated callbacks.
EditDistance.transform(oldChips, newChips, AssistantChip::equals);
} else {
for (int i = 0; i < oldChips.size(); ++i) {
AssistantChip oldChip = oldChips.get(i);
AssistantChip newChip = newChips.get(i);
// We assume that the enabled state is the only thing that may change.
if (oldChip.isDisabled() == newChip.isDisabled()) {
continue;
}
View view = mView.getLayoutManager().findViewByPosition(i);
if (view == null) {
oldChips.update(i, newChip);
} else {
oldChip.setDisabled(newChip.isDisabled());
view.setEnabled(!newChip.isDisabled());
}
}
}
}
/** Changes the visibility of all chips not matching the identifier **/
public void setAllChipsVisibleExcept(String identifier, boolean visible) {
ListModel<AssistantChip> chips = mModel.getChipsModel();
for (int i = 0; i < chips.size(); ++i) {
View view = mView.getLayoutManager().findViewByPosition(i);
if (view != null && !chips.get(i).getIdentifier().equals(identifier)) {
view.setVisibility(visible ? View.VISIBLE : View.GONE);
}
}
}
/**
* Returns |true| if all chips in the list are considered equal, meaning the same except their
* |isDisabled()| state.
*/
private boolean areChipsEqual(ListModel<AssistantChip> oldChips, List<AssistantChip> newChips) {
if (oldChips.size() != newChips.size()) {
return false;
}
for (int i = 0; i < oldChips.size(); ++i) {
AssistantChip oldChip = oldChips.get(i);
AssistantChip newChip = newChips.get(i);
if (!oldChip.equals(newChip)) {
return false;
}
}
return true;
}
// TODO(crbug.com/806868): Handle RTL layouts. // TODO(crbug.com/806868): Handle RTL layouts.
// TODO(crbug.com/806868): Recycle invisible children instead of laying all of them out. // TODO(crbug.com/806868): Recycle invisible children instead of laying all of them out.
static class CustomLayoutManager extends RecyclerView.LayoutManager { static class CustomLayoutManager extends RecyclerView.LayoutManager {
......
...@@ -101,12 +101,28 @@ class AssistantActionsDecoration extends RecyclerView.ItemDecoration { ...@@ -101,12 +101,28 @@ class AssistantActionsDecoration extends RecyclerView.ItemDecoration {
} }
View lastChild = parent.getChildAt(parent.getChildCount() - 1); View lastChild = parent.getChildAt(parent.getChildCount() - 1);
View beforeLastChild = parent.getChildAt(parent.getChildCount() - 2);
mLastChildRect.left = lastChild.getLeft() + lastChild.getTranslationX(); mLastChildRect.left = lastChild.getLeft() + lastChild.getTranslationX();
mLastChildRect.top = lastChild.getTop() + lastChild.getTranslationY() + mVerticalInset; mLastChildRect.top = lastChild.getTop() + lastChild.getTranslationY() + mVerticalInset;
mLastChildRect.right = lastChild.getRight() + lastChild.getTranslationX(); mLastChildRect.right = lastChild.getRight() + lastChild.getTranslationX();
mLastChildRect.bottom = mLastChildRect.bottom =
lastChild.getBottom() + lastChild.getTranslationY() - mVerticalInset; lastChild.getBottom() + lastChild.getTranslationY() - mVerticalInset;
// Early return to avoid drawing last button decoration if no chip overlaps with the cancel
// chip. Note that there are spurious updates where the last chip's position is
// intermittently in the wrong position. The workaround is to check if the combined width of
// all chips fits without scrolling or not.
// TODO(b/144075373): Fix this properly by avoiding the spurious updates.
OrientationHelper orientationHelper = mLayoutManager.mOrientationHelper;
int sumDecoratedWidth = 0;
for (int i = 0; i < parent.getChildCount(); ++i) {
sumDecoratedWidth += orientationHelper.getDecoratedEnd(parent.getChildAt(i))
- orientationHelper.getDecoratedStart(parent.getChildAt(i));
}
if (sumDecoratedWidth < mLayoutManager.getWidth()) {
return;
}
canvas.save(); canvas.save();
// Don't draw on the last child. // Don't draw on the last child.
...@@ -116,7 +132,6 @@ class AssistantActionsDecoration extends RecyclerView.ItemDecoration { ...@@ -116,7 +132,6 @@ class AssistantActionsDecoration extends RecyclerView.ItemDecoration {
canvas.clipPath(mLastChildPath, Region.Op.DIFFERENCE); canvas.clipPath(mLastChildPath, Region.Op.DIFFERENCE);
// Overlay children close to the last child with a semi-transparent paint. // Overlay children close to the last child with a semi-transparent paint.
OrientationHelper orientationHelper = mLayoutManager.mOrientationHelper;
float lastChildRight = orientationHelper.getDecoratedEnd(lastChild); float lastChildRight = orientationHelper.getDecoratedEnd(lastChild);
for (int i = parent.getChildCount() - 2; i >= 0; i--) { for (int i = parent.getChildCount() - 2; i >= 0; i--) {
View child = parent.getChildAt(i); View child = parent.getChildAt(i);
...@@ -136,8 +151,6 @@ class AssistantActionsDecoration extends RecyclerView.ItemDecoration { ...@@ -136,8 +151,6 @@ class AssistantActionsDecoration extends RecyclerView.ItemDecoration {
canvas.drawRect(mChildRect, mOverlayPaint); canvas.drawRect(mChildRect, mOverlayPaint);
} }
View beforeLastChild = parent.getChildAt(parent.getChildCount() - 2);
// Draw a fixed size white-to-transparent linear gradient from left to right. // Draw a fixed size white-to-transparent linear gradient from left to right.
mGradientDrawable.setBounds( mGradientDrawable.setBounds(
0, mVerticalSpacing, mGradientWidth, parent.getHeight() - mVerticalSpacing); 0, mVerticalSpacing, mGradientWidth, parent.getHeight() - mVerticalSpacing);
......
...@@ -4,15 +4,24 @@ ...@@ -4,15 +4,24 @@
package org.chromium.chrome.browser.autofill_assistant.carousel; package org.chromium.chrome.browser.autofill_assistant.carousel;
import org.chromium.ui.modelutil.ListModel; import org.chromium.ui.modelutil.PropertyModel;
import java.util.ArrayList;
import java.util.List;
/** /**
* State for the carousel of the Autofill Assistant. * State for the carousel of the Autofill Assistant.
*/ */
public class AssistantCarouselModel { public class AssistantCarouselModel extends PropertyModel {
private final ListModel<AssistantChip> mChipsModel = new ListModel<>(); public static final WritableObjectPropertyKey<List<AssistantChip>> CHIPS =
new WritableObjectPropertyKey<>();
public AssistantCarouselModel() {
super(CHIPS);
set(CHIPS, new ArrayList<>());
}
public ListModel<AssistantChip> getChipsModel() { public void setChips(List<AssistantChip> chips) {
return mChipsModel; set(CHIPS, chips);
} }
} }
...@@ -58,6 +58,9 @@ public class AssistantChip { ...@@ -58,6 +58,9 @@ public class AssistantChip {
/** Whether this chip is enabled or not. */ /** Whether this chip is enabled or not. */
private boolean mDisabled; private boolean mDisabled;
/** Whether this chip is visible or not. */
private boolean mVisible;
/** /**
* Whether this chip is sticky. A sticky chip will be a candidate to be displayed in the header * Whether this chip is sticky. A sticky chip will be a candidate to be displayed in the header
* if the peek mode of the sheet is HANDLE_HEADER. * if the peek mode of the sheet is HANDLE_HEADER.
...@@ -75,11 +78,23 @@ public class AssistantChip { ...@@ -75,11 +78,23 @@ public class AssistantChip {
mIcon = icon; mIcon = icon;
mText = text; mText = text;
mDisabled = disabled; mDisabled = disabled;
mVisible = true;
mSticky = sticky; mSticky = sticky;
mIdentifier = identifier; mIdentifier = identifier;
mSelectedListener = selectedListener; mSelectedListener = selectedListener;
} }
public AssistantChip(AssistantChip other) {
mType = other.mType;
mIcon = other.mIcon;
mText = other.mText;
mDisabled = other.mDisabled;
mVisible = other.mVisible;
mSticky = other.mSticky;
mIdentifier = other.mIdentifier;
mSelectedListener = other.mSelectedListener;
}
public int getType() { public int getType() {
return mType; return mType;
} }
...@@ -96,15 +111,18 @@ public class AssistantChip { ...@@ -96,15 +111,18 @@ public class AssistantChip {
return mDisabled; return mDisabled;
} }
/** public boolean isVisible() {
* Set the disabled state of the {@link AssistantChip} object. Changing this flag will not return mVisible;
* affect the view this chip is bound to. Use this to keep the model and view in sync, if }
* the view's state changes.
*/
public void setDisabled(boolean disabled) { public void setDisabled(boolean disabled) {
mDisabled = disabled; mDisabled = disabled;
} }
public void setVisible(boolean visible) {
mVisible = visible;
}
public boolean isSticky() { public boolean isSticky() {
return mSticky; return mSticky;
} }
...@@ -119,15 +137,14 @@ public class AssistantChip { ...@@ -119,15 +137,14 @@ public class AssistantChip {
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
if (other == this) {
return true;
}
if (!(other instanceof AssistantChip)) { if (!(other instanceof AssistantChip)) {
return false; return false;
} }
AssistantChip that = (AssistantChip) other; AssistantChip that = (AssistantChip) other;
return this.getType() == that.getType() && this.getText().equals(that.getText()) return this.getType() == that.getType() && this.getText().equals(that.getText())
&& this.getIcon() == that.getIcon() && this.isSticky() == that.isSticky(); && this.getIcon() == that.getIcon() && this.isSticky() == that.isSticky()
&& this.getIdentifier().equals(that.getIdentifier())
&& this.isDisabled() == that.isDisabled() && this.isVisible() == that.isVisible();
} }
} }
// 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.carousel;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.util.DiffUtil;
import android.support.v7.util.ListUpdateCallback;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Custom RecyclerView adapter for instances of {@code AssistantChip}.
*/
public class AssistantChipAdapter extends RecyclerView.Adapter<AssistantChipViewHolder> {
private final List<AssistantChip> mChips = new ArrayList<>();
void setChips(List<AssistantChip> chips) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override
public int getOldListSize() {
return mChips.size();
}
@Override
public int getNewListSize() {
return chips.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
AssistantChip oldChip = mChips.get(oldItemPosition);
AssistantChip newChip = chips.get(newItemPosition);
return newChip.getType() == oldChip.getType()
&& newChip.getText().equals(oldChip.getText())
&& newChip.getIcon() == oldChip.getIcon()
&& newChip.isSticky() == oldChip.isSticky();
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return chips.get(newItemPosition).equals(mChips.get(oldItemPosition));
}
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return chips.get(newItemPosition);
}
});
// TODO(b/144075373): The following should work, but does not fire change notifications
// properly, leading to missing change animations:
// mChips.clear();
// mChips.addAll(chips);
// diffResult.dispatchUpdatesTo(this);
// The workaround is to update manually:
diffResult.dispatchUpdatesTo(new ListUpdateCallback() {
@Override
public void onInserted(int position, int count) {
for (int i = 0; i < count; ++i) {
mChips.add(position + i, chips.get(i));
notifyItemInserted(position);
}
}
@Override
public void onRemoved(int position, int count) {
for (int i = 0; i < count; ++i) {
mChips.remove(position);
notifyItemRemoved(position);
}
}
@Override
public void onMoved(int fromPosition, int toPosition) {
Collections.swap(mChips, fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count, @Nullable Object payload) {
assert payload instanceof AssistantChip;
AssistantChip newChip = (AssistantChip) payload;
mChips.set(position, newChip);
notifyItemChanged(position);
}
});
}
@NonNull
@Override
public AssistantChipViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return AssistantChipViewHolder.create(parent, viewType);
}
@Override
public void onBindViewHolder(@NonNull AssistantChipViewHolder viewHolder, int position) {
viewHolder.bind(mChips.get(position));
}
@Override
public void onBindViewHolder(@NonNull AssistantChipViewHolder viewHolder, int position,
@Nullable List<Object> payloads) {
// Perform in-place update instead of full bind when possible.
if (payloads != null && payloads.size() > 0) {
AssistantChip changedChip = (AssistantChip) payloads.get(payloads.size() - 1);
viewHolder.getView().setEnabled(!changedChip.isDisabled());
viewHolder.getView().setVisibility(changedChip.isVisible() ? View.VISIBLE : View.GONE);
return;
}
onBindViewHolder(viewHolder, position);
}
@Override
public int getItemCount() {
return mChips.size();
}
@Override
public int getItemViewType(int position) {
return mChips.get(position).getType();
}
}
...@@ -30,7 +30,7 @@ public class AssistantChipViewHolder extends ViewHolder { ...@@ -30,7 +30,7 @@ public class AssistantChipViewHolder extends ViewHolder {
public static AssistantChipViewHolder create(ViewGroup parent, int viewType) { public static AssistantChipViewHolder create(ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
ButtonView view = null; ButtonView view = null;
switch (viewType % AssistantChip.Type.NUM_ENTRIES) { switch (viewType) {
case AssistantChip.Type.CHIP_ASSISTIVE: case AssistantChip.Type.CHIP_ASSISTIVE:
view = (ButtonView) layoutInflater.inflate( view = (ButtonView) layoutInflater.inflate(
R.layout.autofill_assistant_button_assistive, /* root= */ null); R.layout.autofill_assistant_button_assistive, /* root= */ null);
...@@ -54,12 +54,6 @@ public class AssistantChipViewHolder extends ViewHolder { ...@@ -54,12 +54,6 @@ public class AssistantChipViewHolder extends ViewHolder {
} }
public static int getViewType(AssistantChip chip) { public static int getViewType(AssistantChip chip) {
// We add AssistantChip.Type.CHIP_TYPE_NUMBER to differentiate between enabled and disabled
// chips of the same type. Ideally, we should return a (type, disabled) tuple but
// RecyclerView does not allow that.
if (chip.isDisabled()) {
return chip.getType() + AssistantChip.Type.NUM_ENTRIES;
}
return chip.getType(); return chip.getType();
} }
...@@ -73,6 +67,7 @@ public class AssistantChipViewHolder extends ViewHolder { ...@@ -73,6 +67,7 @@ public class AssistantChipViewHolder extends ViewHolder {
public void bind(AssistantChip chip) { public void bind(AssistantChip chip) {
mView.setEnabled(!chip.isDisabled()); mView.setEnabled(!chip.isDisabled());
mView.setVisibility(chip.isVisible() ? View.VISIBLE : View.GONE);
String text = chip.getText(); String text = chip.getText();
if (text.isEmpty()) { if (text.isEmpty()) {
......
...@@ -34,6 +34,10 @@ import org.chromium.chrome.browser.customtabs.CustomTabActivityTestRule; ...@@ -34,6 +34,10 @@ import org.chromium.chrome.browser.customtabs.CustomTabActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner; import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.content_public.browser.test.util.TestThreadUtils; import org.chromium.content_public.browser.test.util.TestThreadUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** /**
* Tests for the autofill assistant actions carousel. * Tests for the autofill assistant actions carousel.
*/ */
...@@ -75,7 +79,7 @@ public class AutofillAssistantActionsCarouselUiTest { ...@@ -75,7 +79,7 @@ public class AutofillAssistantActionsCarouselUiTest {
AssistantCarouselModel model = new AssistantCarouselModel(); AssistantCarouselModel model = new AssistantCarouselModel();
AssistantActionsCarouselCoordinator coordinator = createCoordinator(model); AssistantActionsCarouselCoordinator coordinator = createCoordinator(model);
assertThat(model.getChipsModel().size(), is(0)); assertThat(model.get(AssistantCarouselModel.CHIPS).size(), is(0));
assertThat(coordinator.getView().getAdapter().getItemCount(), is(0)); assertThat(coordinator.getView().getAdapter().getItemCount(), is(0));
} }
...@@ -88,9 +92,10 @@ public class AutofillAssistantActionsCarouselUiTest { ...@@ -88,9 +92,10 @@ public class AutofillAssistantActionsCarouselUiTest {
TestThreadUtils.runOnUiThreadBlocking( TestThreadUtils.runOnUiThreadBlocking(
() ()
-> model.getChipsModel().add( -> model.set(AssistantCarouselModel.CHIPS,
new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, Collections.singletonList(new AssistantChip(
AssistantChip.Icon.NONE, "Test", false, true, "", null))); AssistantChip.Type.BUTTON_HAIRLINE, AssistantChip.Icon.NONE,
"Test", false, true, "", null))));
// Chip was created and is displayed on the screen. // Chip was created and is displayed on the screen.
onView(is(coordinator.getView())) onView(is(coordinator.getView()))
...@@ -108,14 +113,15 @@ public class AutofillAssistantActionsCarouselUiTest { ...@@ -108,14 +113,15 @@ public class AutofillAssistantActionsCarouselUiTest {
// Note: this should be a small number that fits on screen without scrolling. // Note: this should be a small number that fits on screen without scrolling.
int numChips = 3; int numChips = 3;
TestThreadUtils.runOnUiThreadBlocking(() -> { List<AssistantChip> chips = new ArrayList<>();
for (int i = 0; i < numChips; i++) { for (int i = 0; i < numChips; i++) {
model.getChipsModel().add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, chips.add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, AssistantChip.Icon.NONE,
AssistantChip.Icon.NONE, "T" + i, false, false, "", null)); "T" + i, false, false, "", null));
} }
model.getChipsModel().add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, chips.add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, AssistantChip.Icon.NONE,
AssistantChip.Icon.NONE, "X", false, true, "", null)); "X", false, true, "", null));
});
TestThreadUtils.runOnUiThreadBlocking(() -> model.set(AssistantCarouselModel.CHIPS, chips));
// Cancel chip is displayed to the user. // Cancel chip is displayed to the user.
onView(withText("X")).check(matches(isDisplayed())); onView(withText("X")).check(matches(isDisplayed()));
...@@ -135,14 +141,14 @@ public class AutofillAssistantActionsCarouselUiTest { ...@@ -135,14 +141,14 @@ public class AutofillAssistantActionsCarouselUiTest {
// Note: this should be a large number that does not fit on screen without scrolling. // Note: this should be a large number that does not fit on screen without scrolling.
int numChips = 30; int numChips = 30;
TestThreadUtils.runOnUiThreadBlocking(() -> { List<AssistantChip> chips = new ArrayList<>();
for (int i = 0; i < numChips; i++) { for (int i = 0; i < numChips; i++) {
model.getChipsModel().add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, chips.add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, AssistantChip.Icon.NONE,
AssistantChip.Icon.NONE, "Test" + i, false, false, "", null)); "Test" + i, false, false, "", null));
} }
model.getChipsModel().add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, chips.add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, AssistantChip.Icon.NONE,
AssistantChip.Icon.NONE, "Cancel", false, true, "", null)); "Cancel", false, true, "", null));
}); TestThreadUtils.runOnUiThreadBlocking(() -> model.set(AssistantCarouselModel.CHIPS, chips));
// Cancel chip is initially displayed to the user. // Cancel chip is initially displayed to the user.
onView(withText("Cancel")).check(matches(isDisplayed())); onView(withText("Cancel")).check(matches(isDisplayed()));
......
...@@ -221,7 +221,7 @@ public class AutofillAssistantUiTest { ...@@ -221,7 +221,7 @@ public class AutofillAssistantUiTest {
new AssistantChip(AssistantChip.Type.CHIP_ASSISTIVE, AssistantChip.Icon.NONE, new AssistantChip(AssistantChip.Type.CHIP_ASSISTIVE, AssistantChip.Icon.NONE,
"chip 1", "chip 1",
/* disabled= */ false, /* sticky= */ false, "", mRunnableMock)); /* disabled= */ false, /* sticky= */ false, "", mRunnableMock));
ThreadUtils.runOnUiThreadBlocking(() -> carouselModel.getChipsModel().set(chips)); ThreadUtils.runOnUiThreadBlocking(() -> carouselModel.setChips(chips));
RecyclerView chipsViewContainer = carouselCoordinator.getView(); RecyclerView chipsViewContainer = carouselCoordinator.getView();
Assert.assertEquals(2, chipsViewContainer.getAdapter().getItemCount()); Assert.assertEquals(2, chipsViewContainer.getAdapter().getItemCount());
......
// Copyright 2019 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 org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.verify;
import android.support.test.filters.SmallTest;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.ui.modelutil.ListModel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Unit test suite for {@link EditDistance}.
*/
@RunWith(JUnit4.class)
public class EditDistanceTest {
@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Test
@SmallTest
public void testEmptySource() {
ListModel<Integer> spiedListModel = createSpiedListModel();
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).add(0, 3);
inOrder.verify(spiedListModel).add(0, 2);
inOrder.verify(spiedListModel).add(0, 1);
}
@Test
@SmallTest
@DisabledTest(message = "crbug.com/963672")
public void testEmptyTarget() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Collections.emptyList());
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).removeAt(2);
inOrder.verify(spiedListModel).removeAt(1);
inOrder.verify(spiedListModel).removeAt(0);
}
@Test
@SmallTest
public void testEmptySourceAndEmptyTarget() {
testTransformation(createSpiedListModel(), Collections.emptyList());
}
@Test
@SmallTest
public void testEqualSourceAndTarget() {
testTransformation(createSpiedListModel(1, 2, 3), Arrays.asList(1, 2, 3));
}
@Test
@SmallTest
public void testInsertBeginning() {
ListModel<Integer> spiedListModel = createSpiedListModel(2, 3);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).add(0, 1);
}
@Test
@SmallTest
public void testInsertMiddle() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 3);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).add(1, 2);
}
@Test
@SmallTest
public void testInsertEnd() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).add(2, 3);
}
@Test
@SmallTest
public void testDeleteBeginning() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Arrays.asList(2, 3));
verify(spiedListModel).removeAt(0);
}
@Test
@SmallTest
public void testDeleteMiddle() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Arrays.asList(1, 3));
verify(spiedListModel).removeAt(1);
}
@Test
@SmallTest
@DisabledTest(message = "crbug.com/963672")
public void testDeleteEnd() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Arrays.asList(1, 2));
verify(spiedListModel).removeAt(2);
}
@Test
@SmallTest
public void testSubstitutionBeginning() {
ListModel<Integer> spiedListModel = createSpiedListModel(0, 2, 3);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).update(0, 1);
}
@Test
@SmallTest
public void testSubstitutionMiddle() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 0, 3);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).update(1, 2);
}
@Test
@SmallTest
public void testSubstitutionEnd() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 0);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).update(2, 3);
}
@Test
@SmallTest
public void testMultipleInsert() {
ListModel<Integer> spiedListModel = createSpiedListModel(3, 6);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8));
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).add(2, 8);
inOrder.verify(spiedListModel).add(2, 7);
inOrder.verify(spiedListModel).add(1, 5);
inOrder.verify(spiedListModel).add(1, 4);
inOrder.verify(spiedListModel).add(0, 2);
inOrder.verify(spiedListModel).add(0, 1);
}
@Test
@SmallTest
@DisabledTest(message = "crbug.com/963672")
public void testMultipleDelete() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3, 4, 5, 6, 7, 8);
testTransformation(spiedListModel, Arrays.asList(3, 6));
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).removeAt(7);
inOrder.verify(spiedListModel).removeAt(6);
inOrder.verify(spiedListModel).removeAt(4);
inOrder.verify(spiedListModel).removeAt(3);
inOrder.verify(spiedListModel).removeAt(1);
inOrder.verify(spiedListModel).removeAt(0);
}
@Test
@SmallTest
public void testMultipleSubstitutions() {
ListModel<Integer> spiedListModel = createSpiedListModel(0, 0, 3, 0, 0, 6, 0, 0);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8));
verify(spiedListModel).update(0, 1);
verify(spiedListModel).update(1, 2);
verify(spiedListModel).update(3, 4);
verify(spiedListModel).update(4, 5);
verify(spiedListModel).update(6, 7);
verify(spiedListModel).update(7, 8);
}
@Test
@SmallTest
public void testPopAndAppend() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Arrays.asList(2, 3, 4));
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).add(3, 4);
inOrder.verify(spiedListModel).removeAt(0);
}
@Test
@SmallTest
public void testAppendAndPop() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Arrays.asList(0, 1, 2));
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).removeAt(2);
inOrder.verify(spiedListModel).add(0, 0);
}
@Test
@SmallTest
public void testMixedOperations() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3, 4, 5, 6);
testTransformation(spiedListModel, Arrays.asList(2, 8, 4, 5, 7, 6));
// 0-cost substitutions.
verify(spiedListModel).update(1, 2);
verify(spiedListModel).update(3, 4);
verify(spiedListModel).update(4, 5);
verify(spiedListModel).update(5, 6);
verify(spiedListModel).update(2, 8);
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).add(5, 7);
inOrder.verify(spiedListModel).removeAt(0);
}
private void testTransformation(ListModel<Integer> source, List<Integer> target) {
EditDistance.transform(source, target, Integer::equals);
List<Integer> sourceList = new ArrayList<>();
for (Integer value : source) {
sourceList.add(value);
}
Assert.assertEquals(target, sourceList);
}
private static ListModel<Integer> createSpiedListModel(Integer... values) {
ListModel<Integer> model = new ListModel<>();
model.set(values);
return Mockito.spy(model);
}
}
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