Commit 9347775e authored by Friedrich Horschig's avatar Friedrich Horschig Committed by Commit Bot

[Android] Make autofill keyboard use clank architecture

This CL reworks the way autofill suggestions are communicated to the
keyboard accessory. Instead of setting an additional view, they behave
like all other actions.
The old accessory adjusted its layout based on the website's layout.
This was removed for now - the system-wide setting is used instead.

Side effect: This seems to resolve most known crashes.

Known issues:
- chip elevation has no effect pre-Lollipop (TODO)

Old bugs:
- inconsistent state of scrollview most likely solved
  (--> no scrollview, no inconsistent state)
- flaky tests passed 30/30 locally times but reenabling will happen
  separately (issue 854224, might still flake on bots)

Demo video:
https://drive.google.com/open?id=1aJbjtTZPP-hPZu2FYA_qHRHqaeHkfWCq

Bug: 853772, 834976
Change-Id: I53ed7a18a72e72f72bae45906ac9bb5ee06992e3
Reviewed-on: https://chromium-review.googlesource.com/1170771Reviewed-by: default avatarBernhard Bauer <bauerb@chromium.org>
Reviewed-by: default avatarVasilii Sukhanov <vasilii@chromium.org>
Commit-Queue: Friedrich Horschig <fhorschig@chromium.org>
Cr-Commit-Position: refs/heads/master@{#582589}
parent 5b2ab789
...@@ -2,14 +2,11 @@ ...@@ -2,14 +2,11 @@
<!-- Copyright 2015 The Chromium Authors. All rights reserved. <!-- Copyright 2015 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. --> found in the LICENSE file. -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<inset xmlns:android="http://schemas.android.com/apk/res/android" <item>
android:insetBottom="@dimen/keyboard_accessory_padding" <shape>
android:insetLeft="@dimen/keyboard_accessory_half_padding" <solid android:color="@android:color/white" />
android:insetRight="@dimen/keyboard_accessory_half_padding" <corners android:radius="@dimen/keyboard_accessory_action_height" />
android:insetTop="@dimen/keyboard_accessory_padding"> </shape>
<shape> </item>
<solid android:color="@color/google_grey_200" /> </layer-list>
<corners android:radius="2dp" />
</shape>
</inset>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2015 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. -->
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="@dimen/keyboard_accessory_height"
android:layout_width="40dp"
android:background="@drawable/autofill_chip_inset"
android:padding="12dp"
tools:ignore="contentDescription" />
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2015 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. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="@dimen/keyboard_accessory_height"
android:layout_width="wrap_content"
android:background="@drawable/autofill_chip_inset"
android:paddingEnd="@dimen/keyboard_accessory_padding"
android:paddingStart="@dimen/keyboard_accessory_padding">
<TextView
android:id="@+id/autofill_keyboard_accessory_item_label"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:drawablePadding="@dimen/keyboard_accessory_half_padding"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
android:gravity="center_vertical"
android:minWidth="@dimen/keyboard_accessory_action_height"
android:paddingEnd="@dimen/keyboard_accessory_half_padding"
android:paddingStart="@dimen/keyboard_accessory_half_padding"
android:singleLine="true"
android:textColor="@color/modern_grey_800"
android:textSize="@dimen/keyboard_accessory_text_size" />
<TextView
android:id="@+id/autofill_keyboard_accessory_item_sublabel"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:ellipsize="end"
android:gravity="center_vertical"
android:paddingEnd="@dimen/keyboard_accessory_half_padding"
android:singleLine="true"
android:textColor="@color/google_grey_600"
android:textSize="@dimen/keyboard_accessory_text_size"
android:visibility="gone" />
</LinearLayout>
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
android:orientation="horizontal" android:orientation="horizontal"
android:layout_height="@dimen/keyboard_accessory_height" android:layout_height="@dimen/keyboard_accessory_height"
android:layout_width="match_parent" android:layout_width="match_parent"
android:paddingEnd="@dimen/keyboard_accessory_padding" android:paddingEnd="0dp"
android:paddingStart="@dimen/keyboard_accessory_padding"> android:paddingStart="@dimen/keyboard_accessory_padding">
<android.support.design.widget.TabLayout <android.support.design.widget.TabLayout
...@@ -30,9 +30,4 @@ ...@@ -30,9 +30,4 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<HorizontalScrollView
android:id="@+id/suggestions_view"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
</org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryView> </org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryView>
...@@ -15,8 +15,9 @@ ...@@ -15,8 +15,9 @@
android:paddingEnd="@dimen/keyboard_accessory_half_padding" android:paddingEnd="@dimen/keyboard_accessory_half_padding"
android:paddingStart="@dimen/keyboard_accessory_half_padding" android:paddingStart="@dimen/keyboard_accessory_half_padding"
android:paddingTop="0dp" android:paddingTop="0dp"
android:layout_marginBottom="@dimen/keyboard_accessory_half_padding"
android:layout_marginTop="@dimen/keyboard_accessory_half_padding"
android:textAlignment="center" android:textAlignment="center"
android:textColor="@color/white_alpha_90" android:textAppearance="@style/WhiteButtonText"
android:textSize="@dimen/keyboard_accessory_text_size"
app:buttonColor="@color/light_active_color" app:buttonColor="@color/light_active_color"
app:buttonRaised="false"/> app:buttonRaised="false"/>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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. -->
<!-- TODO(fhorschig): android:elevation has no effect pre-Lollipop. Possibly, using a CardView or
holo drawables could create compatibility - if this becomes the final design.-->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:gravity="center"
android:layout_height="@dimen/keyboard_accessory_action_height"
android:layout_width="wrap_content"
android:minHeight="0dp"
android:minWidth="0dp"
android:paddingBottom="0dp"
android:paddingEnd="@dimen/keyboard_accessory_half_padding"
android:paddingStart="@dimen/keyboard_accessory_half_padding"
android:paddingTop="0dp"
android:layout_marginBottom="@dimen/keyboard_accessory_half_padding"
android:layout_marginTop="@dimen/keyboard_accessory_half_padding"
android:elevation="2dp"
android:textAppearance="@style/BlackTitle2"
android:background="@drawable/autofill_chip_inset"/>
...@@ -129,7 +129,6 @@ ...@@ -129,7 +129,6 @@
<dimen name="keyboard_accessory_height">48dp</dimen> <dimen name="keyboard_accessory_height">48dp</dimen>
<dimen name="keyboard_accessory_padding">6dp</dimen> <dimen name="keyboard_accessory_padding">6dp</dimen>
<dimen name="keyboard_accessory_sheet_height">330dp</dimen> <dimen name="keyboard_accessory_sheet_height">330dp</dimen>
<dimen name="keyboard_accessory_text_size">14sp</dimen>
<dimen name="keyboard_accessory_suggestion_margin">16dp</dimen> <dimen name="keyboard_accessory_suggestion_margin">16dp</dimen>
<dimen name="keyboard_accessory_suggestion_height">48dp</dimen> <dimen name="keyboard_accessory_suggestion_height">48dp</dimen>
<dimen name="keyboard_accessory_suggestion_icon_size">20dp</dimen> <dimen name="keyboard_accessory_suggestion_icon_size">20dp</dimen>
......
...@@ -13,12 +13,19 @@ import org.chromium.base.annotations.JNINamespace; ...@@ -13,12 +13,19 @@ import org.chromium.base.annotations.JNINamespace;
import org.chromium.chrome.R; import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity; import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ResourceId; import org.chromium.chrome.browser.ResourceId;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryCoordinator; import org.chromium.chrome.browser.autofill.keyboard_accessory.AccessoryAction;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryMetricsRecorder;
import org.chromium.chrome.browser.autofill.keyboard_accessory.ManualFillingCoordinator;
import org.chromium.components.autofill.AutofillDelegate; import org.chromium.components.autofill.AutofillDelegate;
import org.chromium.components.autofill.AutofillSuggestion; import org.chromium.components.autofill.AutofillSuggestion;
import org.chromium.components.autofill.PopupItemId;
import org.chromium.ui.DropdownItem; import org.chromium.ui.DropdownItem;
import org.chromium.ui.base.WindowAndroid; import org.chromium.ui.base.WindowAndroid;
import java.util.ArrayList;
import java.util.List;
/** /**
* JNI call glue for AutofillExternalDelagate C++ and Java objects. * JNI call glue for AutofillExternalDelagate C++ and Java objects.
* This provides an alternative UI for Autofill suggestions, and replaces AutofillPopupBridge when * This provides an alternative UI for Autofill suggestions, and replaces AutofillPopupBridge when
...@@ -28,9 +35,10 @@ import org.chromium.ui.base.WindowAndroid; ...@@ -28,9 +35,10 @@ import org.chromium.ui.base.WindowAndroid;
public class AutofillKeyboardAccessoryBridge public class AutofillKeyboardAccessoryBridge
implements AutofillDelegate, DialogInterface.OnClickListener { implements AutofillDelegate, DialogInterface.OnClickListener {
private long mNativeAutofillKeyboardAccessory; private long mNativeAutofillKeyboardAccessory;
private KeyboardAccessoryCoordinator mKeyboardAccessory; private ManualFillingCoordinator mManualFillingCoordinator;
private Context mContext; private Context mContext;
private AutofillKeyboardSuggestions mAutofillSuggestions; private KeyboardAccessoryData.Provider<KeyboardAccessoryData.Action> mChipProvider =
new KeyboardAccessoryData.PropertyProvider<>(AccessoryAction.AUTOFILL_SUGGESTION);
private AutofillKeyboardAccessoryBridge() { private AutofillKeyboardAccessoryBridge() {
} }
...@@ -48,6 +56,9 @@ public class AutofillKeyboardAccessoryBridge ...@@ -48,6 +56,9 @@ public class AutofillKeyboardAccessoryBridge
@Override @Override
public void suggestionSelected(int listIndex) { public void suggestionSelected(int listIndex) {
KeyboardAccessoryMetricsRecorder.recordActionSelected(
AccessoryAction.GENERATE_PASSWORD_AUTOMATIC);
if (mManualFillingCoordinator != null) mManualFillingCoordinator.dismiss();
if (mNativeAutofillKeyboardAccessory == 0) return; if (mNativeAutofillKeyboardAccessory == 0) return;
nativeSuggestionSelected(mNativeAutofillKeyboardAccessory, listIndex); nativeSuggestionSelected(mNativeAutofillKeyboardAccessory, listIndex);
} }
...@@ -85,12 +96,12 @@ public class AutofillKeyboardAccessoryBridge ...@@ -85,12 +96,12 @@ public class AutofillKeyboardAccessoryBridge
mContext = windowAndroid.getActivity().get(); mContext = windowAndroid.getActivity().get();
assert mContext != null; assert mContext != null;
if (mContext instanceof ChromeActivity) { if (mContext instanceof ChromeActivity) {
mKeyboardAccessory = ((ChromeActivity) mContext).getKeyboardAccessory(); mManualFillingCoordinator = ((ChromeActivity) mContext).getManualFillingController();
mManualFillingCoordinator.getKeyboardAccessory().registerActionListProvider(
mChipProvider);
} }
mNativeAutofillKeyboardAccessory = nativeAutofillKeyboardAccessory; mNativeAutofillKeyboardAccessory = nativeAutofillKeyboardAccessory;
mAutofillSuggestions =
new AutofillKeyboardSuggestions(windowAndroid, this, shouldLimitLabelWidth);
} }
/** /**
...@@ -106,10 +117,9 @@ public class AutofillKeyboardAccessoryBridge ...@@ -106,10 +117,9 @@ public class AutofillKeyboardAccessoryBridge
*/ */
@CalledByNative @CalledByNative
private void dismiss() { private void dismiss() {
if (mKeyboardAccessory != null) mKeyboardAccessory.dismiss(); mChipProvider.notifyObservers(new KeyboardAccessoryData.Action[0]);
mContext = null; mContext = null;
mKeyboardAccessory = null; mManualFillingCoordinator = null;
mAutofillSuggestions = null;
} }
/** /**
...@@ -118,8 +128,27 @@ public class AutofillKeyboardAccessoryBridge ...@@ -118,8 +128,27 @@ public class AutofillKeyboardAccessoryBridge
*/ */
@CalledByNative @CalledByNative
private void show(AutofillSuggestion[] suggestions, boolean isRtl) { private void show(AutofillSuggestion[] suggestions, boolean isRtl) {
if (mAutofillSuggestions != null) mAutofillSuggestions.setSuggestions(suggestions, isRtl); mChipProvider.notifyObservers(convertSuggestionsToChips(suggestions));
if (mKeyboardAccessory != null) mKeyboardAccessory.setSuggestions(mAutofillSuggestions); }
private KeyboardAccessoryData.Action[] convertSuggestionsToChips(
AutofillSuggestion[] suggestions) {
List<KeyboardAccessoryData.Action> suggestionChips = new ArrayList<>();
for (int i = 0; i < suggestions.length; ++i) {
AutofillSuggestion suggestion = suggestions[i];
// The accessory doesn't need any special options like clearing or managing for now.
if (suggestion.getSuggestionId() == PopupItemId.ITEM_ID_ALL_SAVED_PASSWORDS_ENTRY
|| suggestion.getSuggestionId() == PopupItemId.ITEM_ID_CLEAR_FORM
|| suggestion.getSuggestionId() == PopupItemId.ITEM_ID_SEPARATOR
|| suggestion.getSuggestionId() == PopupItemId.ITEM_ID_AUTOFILL_OPTIONS) {
continue;
}
final int triggerPosition = i;
suggestionChips.add(new KeyboardAccessoryData.Action(suggestion.getLabel(),
AccessoryAction.AUTOFILL_SUGGESTION,
result -> suggestionSelected(triggerPosition)));
}
return suggestionChips.toArray(new KeyboardAccessoryData.Action[suggestionChips.size()]);
} }
// Helper methods for AutofillSuggestion. These are copied from AutofillPopupBridge (which // Helper methods for AutofillSuggestion. These are copied from AutofillPopupBridge (which
......
// 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;
import android.graphics.PorterDuff;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v7.content.res.AppCompatResources;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.chrome.R;
import org.chromium.components.autofill.AutofillDelegate;
import org.chromium.components.autofill.AutofillSuggestion;
import org.chromium.ui.base.WindowAndroid;
/**
* The lists that shows autofill suggestions in the keyboard accessory.
*/
public class AutofillKeyboardSuggestions
extends LinearLayout implements View.OnClickListener, View.OnLongClickListener {
private final WindowAndroid mWindowAndroid;
private final AutofillDelegate mAutofillDelegate;
// If |mMaximumLabelWidthPx| is 0, we do not call |setMaxWidth| on the |TextView| for a
// fillable suggestion label.
private final int mMaximumLabelWidthPx;
private final int mMaximumSublabelWidthPx;
/**
* Creates an AutofillKeyboardAccessory with specified parameters.
* @param windowAndroid The owning WindowAndroid.
* @param autofillDelegate A object that handles the calls to the native
* AutofillKeyboardAccessoryView.
* @param shouldLimitLabelWidth If true, limit suggestion label width to 1/2 device's width.
*/
public AutofillKeyboardSuggestions(WindowAndroid windowAndroid,
AutofillDelegate autofillDelegate, boolean shouldLimitLabelWidth) {
super(windowAndroid.getActivity().get());
assert windowAndroid.getActivity().get() != null;
assert autofillDelegate != null;
mAutofillDelegate = autofillDelegate;
mWindowAndroid = windowAndroid;
int deviceWidthPx = windowAndroid.getDisplay().getDisplayWidth();
mMaximumLabelWidthPx = shouldLimitLabelWidth ? deviceWidthPx / 2 : 0;
mMaximumSublabelWidthPx = deviceWidthPx / 4;
int horizontalPaddingPx =
getResources().getDimensionPixelSize(R.dimen.keyboard_accessory_half_padding);
setPadding(horizontalPaddingPx, 0, horizontalPaddingPx, 0);
}
/**
* @param isRtl Gives the layout direction for the <input> field.
*/
public void setSuggestions(AutofillSuggestion[] suggestions, boolean isRtl) {
assert suggestions.length > 0;
removeAllViews();
// The first suggestion may be a hint to call attention to the keyboard accessory. See
// |IsHintEnabledInKeyboardAccessory|. A 'hint' suggestion does not have a label and is
// not fillable, but has an icon.
final boolean isFirstSuggestionAHint = TextUtils.isEmpty(suggestions[0].getLabel());
if (isFirstSuggestionAHint) {
assert suggestions[0].getIconId() != 0 && !suggestions[0].isFillable();
}
int separatorPosition = -1;
int startIndex = isRtl ? suggestions.length - 1 : 0;
int endIndex = isRtl ? -1 : suggestions.length; // The index after the last element.
int i = startIndex;
while (i != endIndex) {
AutofillSuggestion suggestion = suggestions[i];
boolean isKeyboardAccessoryHint = i == 0 && isFirstSuggestionAHint;
if (!isKeyboardAccessoryHint) {
assert !TextUtils.isEmpty(suggestion.getLabel());
}
View touchTarget;
if (suggestion.isFillable() || suggestion.getIconId() == 0) {
touchTarget = createAccessoryItem(suggestion);
} else {
if (separatorPosition == -1 && !isKeyboardAccessoryHint) separatorPosition = i;
touchTarget = createAccessoryIcon(suggestion, isKeyboardAccessoryHint);
}
if (!isKeyboardAccessoryHint) {
touchTarget.setTag(i);
touchTarget.setOnClickListener(this);
if (suggestion.isDeletable()) {
touchTarget.setOnLongClickListener(this);
}
}
addView(touchTarget);
i = isRtl ? i - 1 : i + 1;
}
if (separatorPosition != -1) {
addView(createSeparatorView(), separatorPosition);
}
}
@NonNull
private View createAccessoryItem(AutofillSuggestion suggestion) {
View touchTarget;
touchTarget = LayoutInflater.from(getContext())
.inflate(R.layout.autofill_keyboard_accessory_item, this, false);
TextView label =
(TextView) touchTarget.findViewById(R.id.autofill_keyboard_accessory_item_label);
if (mMaximumLabelWidthPx > 0 && suggestion.isFillable()) {
label.setMaxWidth(mMaximumLabelWidthPx);
}
label.setText(suggestion.getLabel());
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
label.setTypeface(Typeface.DEFAULT_BOLD);
}
if (suggestion.getIconId() != 0) {
ApiCompatibilityUtils.setCompoundDrawablesRelativeWithIntrinsicBounds(label,
AppCompatResources.getDrawable(getContext(), suggestion.getIconId()),
null /* top */, null /* end */, null /* bottom */);
}
if (!TextUtils.isEmpty(suggestion.getSublabel())) {
assert suggestion.isFillable();
TextView sublabel = (TextView) touchTarget.findViewById(
R.id.autofill_keyboard_accessory_item_sublabel);
sublabel.setText(suggestion.getSublabel());
sublabel.setVisibility(View.VISIBLE);
sublabel.setMaxWidth(mMaximumSublabelWidthPx);
}
return touchTarget;
}
@NonNull
private View createAccessoryIcon(
AutofillSuggestion suggestion, boolean isKeyboardAccessoryHint) {
View touchTarget;
touchTarget = LayoutInflater.from(getContext())
.inflate(R.layout.autofill_keyboard_accessory_icon, this, false);
ImageView icon = (ImageView) touchTarget;
Drawable drawable = AppCompatResources.getDrawable(getContext(), suggestion.getIconId());
if (isKeyboardAccessoryHint) {
drawable.setColorFilter(
ApiCompatibilityUtils.getColor(getResources(), R.color.default_icon_color_blue),
PorterDuff.Mode.SRC_IN);
} else {
icon.setContentDescription(suggestion.getLabel());
}
icon.setImageDrawable(drawable);
return touchTarget;
}
public void dismiss() {
removeAllViews();
mAutofillDelegate.dismissed();
}
@Override
public void onClick(View v) {
mAutofillDelegate.suggestionSelected((int) v.getTag());
}
@Override
public boolean onLongClick(View v) {
mAutofillDelegate.deleteSuggestion((int) v.getTag());
return true;
}
// Helper to create separator view so that the settings icon is aligned to the right of the
// screen.
private View createSeparatorView() {
View separator = new View(getContext());
// Specify a layout weight so that the settings icon, which is displayed after the
// separator, is aligned with the edge of the viewport.
separator.setLayoutParams(new LinearLayout.LayoutParams(0, 0, 1));
return separator;
}
}
...@@ -8,7 +8,6 @@ import android.support.v4.view.ViewPager; ...@@ -8,7 +8,6 @@ import android.support.v4.view.ViewPager;
import android.view.ViewStub; import android.view.ViewStub;
import org.chromium.base.VisibleForTesting; import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.autofill.AutofillKeyboardSuggestions;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryViewBinder.ActionViewHolder; import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryViewBinder.ActionViewHolder;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryViewBinder.TabViewBinder; import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryViewBinder.TabViewBinder;
import org.chromium.chrome.browser.modelutil.LazyViewBinderAdapter; import org.chromium.chrome.browser.modelutil.LazyViewBinderAdapter;
...@@ -85,7 +84,8 @@ public class KeyboardAccessoryCoordinator { ...@@ -85,7 +84,8 @@ public class KeyboardAccessoryCoordinator {
static RecyclerViewAdapter<ActionViewHolder, Void> createActionsAdapter( static RecyclerViewAdapter<ActionViewHolder, Void> createActionsAdapter(
KeyboardAccessoryModel model) { KeyboardAccessoryModel model) {
return new RecyclerViewAdapter<>( return new RecyclerViewAdapter<>(
new SimpleRecyclerViewMcp<>(model.getActionList(), null, ActionViewHolder::bind), new SimpleRecyclerViewMcp<>(model.getActionList(),
KeyboardAccessoryData.Action::getActionType, ActionViewHolder::bind),
ActionViewHolder::create); ActionViewHolder::create);
} }
...@@ -160,15 +160,6 @@ public class KeyboardAccessoryCoordinator { ...@@ -160,15 +160,6 @@ public class KeyboardAccessoryCoordinator {
mMediator.destroy(); mMediator.destroy();
} }
/**
* TODO(fhorschig): Remove this function. The suggestions bridge should become a provider.
* Sets a View that will be displayed in a scroll view at the end of the accessory.
* @param suggestions The suggestions to be rendered into the accessory.
*/
public void setSuggestions(AutofillKeyboardSuggestions suggestions) {
mMediator.setSuggestions(suggestions);
}
/** /**
* Dismisses the accessory by hiding it's view, clearing potentially left over suggestions and * Dismisses the accessory by hiding it's view, clearing potentially left over suggestions and
* hiding the keyboard. * hiding the keyboard.
......
...@@ -44,13 +44,16 @@ public class KeyboardAccessoryData { ...@@ -44,13 +44,16 @@ public class KeyboardAccessoryData {
* @param <T> An {@link Action}, {@link Tab} or {@link Item} that this instance observes. * @param <T> An {@link Action}, {@link Tab} or {@link Item} that this instance observes.
*/ */
public interface Observer<T> { public interface Observer<T> {
int DEFAULT_TYPE = Integer.MIN_VALUE;
/** /**
* A provider calls this function with a list of items that should be available in the * A provider calls this function with a list of items that should be available in the
* keyboard accessory. * keyboard accessory.
* @param actions The actions to be displayed in the Accessory. It's a native array as the * @param typeId Specifies which type of item this update affects.
* @param items The items to be displayed in the Accessory. It's a native array as the
* provider is typically a bridge called via JNI which prefers native types. * provider is typically a bridge called via JNI which prefers native types.
*/ */
void onItemsAvailable(T[] actions); void onItemsAvailable(int typeId, T[] items);
} }
/** /**
...@@ -306,6 +309,15 @@ public class KeyboardAccessoryData { ...@@ -306,6 +309,15 @@ public class KeyboardAccessoryData {
*/ */
public static class PropertyProvider<T> implements Provider<T> { public static class PropertyProvider<T> implements Provider<T> {
private final List<Observer<T>> mObservers = new ArrayList<>(); private final List<Observer<T>> mObservers = new ArrayList<>();
protected int mType;
public PropertyProvider() {
this(Observer.DEFAULT_TYPE);
}
public PropertyProvider(int type) {
mType = type;
}
@Override @Override
public void addObserver(Observer<T> observer) { public void addObserver(Observer<T> observer) {
...@@ -315,7 +327,7 @@ public class KeyboardAccessoryData { ...@@ -315,7 +327,7 @@ public class KeyboardAccessoryData {
@Override @Override
public void notifyObservers(T[] items) { public void notifyObservers(T[] items) {
for (Observer<T> observer : mObservers) { for (Observer<T> observer : mObservers) {
observer.onItemsAvailable(items); observer.onItemsAvailable(mType, items);
} }
} }
} }
......
...@@ -10,13 +10,16 @@ import android.support.annotation.Nullable; ...@@ -10,13 +10,16 @@ import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout; import android.support.design.widget.TabLayout;
import org.chromium.base.VisibleForTesting; import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.autofill.AutofillKeyboardSuggestions;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryCoordinator.VisibilityDelegate; import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryCoordinator.VisibilityDelegate;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Action; import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Action;
import org.chromium.chrome.browser.modelutil.ListObservable; import org.chromium.chrome.browser.modelutil.ListObservable;
import org.chromium.chrome.browser.modelutil.PropertyObservable; import org.chromium.chrome.browser.modelutil.PropertyObservable;
import org.chromium.ui.base.WindowAndroid; import org.chromium.ui.base.WindowAndroid;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/** /**
* This is the second part of the controller of the keyboard accessory component. * This is the second part of the controller of the keyboard accessory component.
* It is responsible to update the {@link KeyboardAccessoryModel} based on Backend calls and notify * It is responsible to update the {@link KeyboardAccessoryModel} based on Backend calls and notify
...@@ -58,8 +61,19 @@ class KeyboardAccessoryMediator ...@@ -58,8 +61,19 @@ class KeyboardAccessoryMediator
} }
@Override @Override
public void onItemsAvailable(KeyboardAccessoryData.Action[] actions) { public void onItemsAvailable(int typeId, KeyboardAccessoryData.Action[] actions) {
mModel.setActions(actions); assert typeId != DEFAULT_TYPE : "Did not specify which Action type has been updated.";
// If there is a new list, retain all actions that are of a different type than the provided
// actions.
List<Action> retainedActions = new ArrayList<>();
for (Action a : mModel.getActionList()) {
if (a.getActionType() == typeId) continue;
retainedActions.add(a);
}
// Always append autofill suggestions to the very end.
int insertPos = typeId == AccessoryAction.AUTOFILL_SUGGESTION ? retainedActions.size() : 0;
retainedActions.addAll(insertPos, Arrays.asList(actions));
mModel.setActions(retainedActions.toArray(new Action[retainedActions.size()]));
} }
@Override @Override
...@@ -81,16 +95,8 @@ class KeyboardAccessoryMediator ...@@ -81,16 +95,8 @@ class KeyboardAccessoryMediator
mModel.getTabList().set(tabs); mModel.getTabList().set(tabs);
} }
void setSuggestions(AutofillKeyboardSuggestions suggestions) {
mModel.setAutofillSuggestions(suggestions);
}
void dismiss() { void dismiss() {
mModel.setActiveTab(null); mModel.setActiveTab(null);
if (mModel.getAutofillSuggestions() != null) {
mModel.getAutofillSuggestions().dismiss();
mModel.setAutofillSuggestions(null);
}
updateVisibility(); updateVisibility();
} }
...@@ -131,7 +137,8 @@ class KeyboardAccessoryMediator ...@@ -131,7 +137,8 @@ class KeyboardAccessoryMediator
// When the accessory just (dis)appeared, there should be no active tab. // When the accessory just (dis)appeared, there should be no active tab.
mModel.setActiveTab(null); mModel.setActiveTab(null);
if (!mModel.isVisible()) { if (!mModel.isVisible()) {
mModel.setActions(new Action[0]); // TODO(fhorschig|ioanap): Maybe the generation bridge should take care of that.
onItemsAvailable(AccessoryAction.GENERATE_PASSWORD_AUTOMATIC, new Action[0]);
} }
return; return;
} }
...@@ -148,10 +155,6 @@ class KeyboardAccessoryMediator ...@@ -148,10 +155,6 @@ class KeyboardAccessoryMediator
if (propertyKey == KeyboardAccessoryModel.PropertyKey.TAB_SELECTION_CALLBACKS) { if (propertyKey == KeyboardAccessoryModel.PropertyKey.TAB_SELECTION_CALLBACKS) {
return; return;
} }
if (propertyKey == KeyboardAccessoryModel.PropertyKey.SUGGESTIONS) {
updateVisibility();
return;
}
assert false : "Every property update needs to be handled explicitly!"; assert false : "Every property update needs to be handled explicitly!";
} }
...@@ -177,8 +180,7 @@ class KeyboardAccessoryMediator ...@@ -177,8 +180,7 @@ class KeyboardAccessoryMediator
private boolean shouldShowAccessory() { private boolean shouldShowAccessory() {
if (!mIsKeyboardVisible && mModel.activeTab() == null) return false; if (!mIsKeyboardVisible && mModel.activeTab() == null) return false;
return mModel.getAutofillSuggestions() != null || mModel.getActionList().size() > 0 return mModel.getActionList().size() > 0 || mModel.getTabList().size() > 0;
|| mModel.getTabList().size() > 0;
} }
private void updateVisibility() { private void updateVisibility() {
......
...@@ -12,15 +12,18 @@ import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessory ...@@ -12,15 +12,18 @@ import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessory
import org.chromium.chrome.browser.modelutil.ListObservable; import org.chromium.chrome.browser.modelutil.ListObservable;
import org.chromium.chrome.browser.modelutil.SimpleListObservable; import org.chromium.chrome.browser.modelutil.SimpleListObservable;
import java.util.HashSet;
import java.util.Set;
/** /**
* This class provides helpers to record metrics related to the keyboard accessory and its sheets. * This class provides helpers to record metrics related to the keyboard accessory and its sheets.
* It can set up observers to observe {@link KeyboardAccessoryModel}s, {@link AccessorySheetModel}s * It can set up observers to observe {@link KeyboardAccessoryModel}s, {@link AccessorySheetModel}s
* or {@link ListObservable<Item>}s changes and records metrics accordingly. * or {@link ListObservable<Item>}s changes and records metrics accordingly.
*/ */
class KeyboardAccessoryMetricsRecorder { public class KeyboardAccessoryMetricsRecorder {
static final String UMA_KEYBOARD_ACCESSORY_ACTION_IMPRESSION = static final String UMA_KEYBOARD_ACCESSORY_ACTION_IMPRESSION =
"KeyboardAccessory.AccessoryActionImpression"; "KeyboardAccessory.AccessoryActionImpression";
static final String UMA_KEYBOARD_ACCESSORY_ACTION_SELECTED = public static final String UMA_KEYBOARD_ACCESSORY_ACTION_SELECTED =
"KeyboardAccessory.AccessoryActionSelected"; "KeyboardAccessory.AccessoryActionSelected";
static final String UMA_KEYBOARD_ACCESSORY_BAR_SHOWN = "KeyboardAccessory.AccessoryBarShown"; static final String UMA_KEYBOARD_ACCESSORY_BAR_SHOWN = "KeyboardAccessory.AccessoryBarShown";
static final String UMA_KEYBOARD_ACCESSORY_SHEET_SUGGESTIONS = static final String UMA_KEYBOARD_ACCESSORY_SHEET_SUGGESTIONS =
...@@ -46,8 +49,7 @@ class KeyboardAccessoryMetricsRecorder { ...@@ -46,8 +49,7 @@ class KeyboardAccessoryMetricsRecorder {
return; return;
} }
if (propertyKey == KeyboardAccessoryModel.PropertyKey.ACTIVE_TAB if (propertyKey == KeyboardAccessoryModel.PropertyKey.ACTIVE_TAB
|| propertyKey == KeyboardAccessoryModel.PropertyKey.TAB_SELECTION_CALLBACKS || propertyKey == KeyboardAccessoryModel.PropertyKey.TAB_SELECTION_CALLBACKS) {
|| propertyKey == KeyboardAccessoryModel.PropertyKey.SUGGESTIONS) {
return; return;
} }
assert false : "Every property update needs to be handled explicitly!"; assert false : "Every property update needs to be handled explicitly!";
...@@ -107,7 +109,7 @@ class KeyboardAccessoryMetricsRecorder { ...@@ -107,7 +109,7 @@ class KeyboardAccessoryMetricsRecorder {
UMA_KEYBOARD_ACCESSORY_ACTION_IMPRESSION, bucket, AccessoryAction.COUNT); UMA_KEYBOARD_ACCESSORY_ACTION_IMPRESSION, bucket, AccessoryAction.COUNT);
} }
static void recordActionSelected(@AccessoryAction int bucket) { public static void recordActionSelected(@AccessoryAction int bucket) {
RecordHistogram.recordEnumeratedHistogram( RecordHistogram.recordEnumeratedHistogram(
UMA_KEYBOARD_ACCESSORY_ACTION_SELECTED, bucket, AccessoryAction.COUNT); UMA_KEYBOARD_ACCESSORY_ACTION_SELECTED, bucket, AccessoryAction.COUNT);
} }
...@@ -163,9 +165,12 @@ class KeyboardAccessoryMetricsRecorder { ...@@ -163,9 +165,12 @@ class KeyboardAccessoryMetricsRecorder {
case AccessoryBarContents.WITH_TABS: case AccessoryBarContents.WITH_TABS:
return keyboardAccessoryModel.getTabList().size() > 0; return keyboardAccessoryModel.getTabList().size() > 0;
case AccessoryBarContents.WITH_ACTIONS: case AccessoryBarContents.WITH_ACTIONS:
return keyboardAccessoryModel.getActionList().size() > 0; return hasAtLeastOneActionOfType(keyboardAccessoryModel.getActionList(),
AccessoryAction.MANAGE_PASSWORDS,
AccessoryAction.GENERATE_PASSWORD_AUTOMATIC);
case AccessoryBarContents.WITH_AUTOFILL_SUGGESTIONS: case AccessoryBarContents.WITH_AUTOFILL_SUGGESTIONS:
return keyboardAccessoryModel.getAutofillSuggestions() != null; return hasAtLeastOneActionOfType(keyboardAccessoryModel.getActionList(),
AccessoryAction.AUTOFILL_SUGGESTION);
case AccessoryBarContents.ANY_CONTENTS: // Intentional fallthrough. case AccessoryBarContents.ANY_CONTENTS: // Intentional fallthrough.
case AccessoryBarContents.NO_CONTENTS: case AccessoryBarContents.NO_CONTENTS:
return false; // General impression is logged last. return false; // General impression is logged last.
...@@ -173,4 +178,15 @@ class KeyboardAccessoryMetricsRecorder { ...@@ -173,4 +178,15 @@ class KeyboardAccessoryMetricsRecorder {
assert false : "Did not check whether to record an impression bucket " + bucket + "."; assert false : "Did not check whether to record an impression bucket " + bucket + ".";
return false; return false;
} }
private static boolean hasAtLeastOneActionOfType(
SimpleListObservable<KeyboardAccessoryData.Action> actionList,
@AccessoryAction int... types) {
Set<Integer> typeList = new HashSet<>(types.length);
for (@AccessoryAction int type : types) typeList.add(type);
for (KeyboardAccessoryData.Action action : actionList) {
if (typeList.contains(action.getActionType())) return true;
}
return false;
}
} }
...@@ -7,7 +7,6 @@ package org.chromium.chrome.browser.autofill.keyboard_accessory; ...@@ -7,7 +7,6 @@ package org.chromium.chrome.browser.autofill.keyboard_accessory;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout; import android.support.design.widget.TabLayout;
import org.chromium.chrome.browser.autofill.AutofillKeyboardSuggestions;
import org.chromium.chrome.browser.modelutil.ListObservable; import org.chromium.chrome.browser.modelutil.ListObservable;
import org.chromium.chrome.browser.modelutil.PropertyObservable; import org.chromium.chrome.browser.modelutil.PropertyObservable;
import org.chromium.chrome.browser.modelutil.SimpleListObservable; import org.chromium.chrome.browser.modelutil.SimpleListObservable;
...@@ -30,7 +29,6 @@ class KeyboardAccessoryModel extends PropertyObservable<KeyboardAccessoryModel.P ...@@ -30,7 +29,6 @@ class KeyboardAccessoryModel extends PropertyObservable<KeyboardAccessoryModel.P
static final List<PropertyKey> ALL_PROPERTIES = new ArrayList<>(); static final List<PropertyKey> ALL_PROPERTIES = new ArrayList<>();
static final PropertyKey VISIBLE = new PropertyKey(); static final PropertyKey VISIBLE = new PropertyKey();
static final PropertyKey SUGGESTIONS = new PropertyKey();
static final PropertyKey ACTIVE_TAB = new PropertyKey(); static final PropertyKey ACTIVE_TAB = new PropertyKey();
static final PropertyKey TAB_SELECTION_CALLBACKS = new PropertyKey(); static final PropertyKey TAB_SELECTION_CALLBACKS = new PropertyKey();
...@@ -45,9 +43,6 @@ class KeyboardAccessoryModel extends PropertyObservable<KeyboardAccessoryModel.P ...@@ -45,9 +43,6 @@ class KeyboardAccessoryModel extends PropertyObservable<KeyboardAccessoryModel.P
private @Nullable Integer mActiveTab; private @Nullable Integer mActiveTab;
private TabLayout.OnTabSelectedListener mTabSelectionCallbacks; private TabLayout.OnTabSelectedListener mTabSelectionCallbacks;
// TODO(fhorschig): Ideally, make this a ListObservable populating a RecyclerView.
private AutofillKeyboardSuggestions mAutofillSuggestions;
KeyboardAccessoryModel() { KeyboardAccessoryModel() {
mActionListObservable = new SimpleListObservable<>(); mActionListObservable = new SimpleListObservable<>();
mTabListObservable = new SimpleListObservable<>(); mTabListObservable = new SimpleListObservable<>();
...@@ -113,14 +108,4 @@ class KeyboardAccessoryModel extends PropertyObservable<KeyboardAccessoryModel.P ...@@ -113,14 +108,4 @@ class KeyboardAccessoryModel extends PropertyObservable<KeyboardAccessoryModel.P
mTabSelectionCallbacks = tabSelectionCallbacks; mTabSelectionCallbacks = tabSelectionCallbacks;
notifyPropertyChanged(PropertyKey.TAB_SELECTION_CALLBACKS); notifyPropertyChanged(PropertyKey.TAB_SELECTION_CALLBACKS);
} }
AutofillKeyboardSuggestions getAutofillSuggestions() {
return mAutofillSuggestions;
}
void setAutofillSuggestions(AutofillKeyboardSuggestions autofillSuggestions) {
if (autofillSuggestions == mAutofillSuggestions) return; // Nothing to do: same object.
mAutofillSuggestions = autofillSuggestions;
notifyPropertyChanged(PropertyKey.SUGGESTIONS);
}
} }
...@@ -19,21 +19,16 @@ import android.util.AttributeSet; ...@@ -19,21 +19,16 @@ import android.util.AttributeSet;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityEvent;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import org.chromium.base.ApiCompatibilityUtils; import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.chrome.R; import org.chromium.chrome.R;
import org.chromium.chrome.browser.autofill.AutofillKeyboardSuggestions;
import javax.annotation.Nullable;
/** /**
* The Accessory sitting above the keyboard and below the content area. It is used for autofill * The Accessory sitting above the keyboard and below the content area. It is used for autofill
* suggestions and manual entry points assisting the user in filling forms. * suggestions and manual entry points assisting the user in filling forms.
*/ */
class KeyboardAccessoryView extends LinearLayout { class KeyboardAccessoryView extends LinearLayout {
private HorizontalScrollView mSuggestionsView;
private RecyclerView mActionsView; private RecyclerView mActionsView;
private TabLayout mTabLayout; private TabLayout mTabLayout;
private TabLayout.TabLayoutOnPageChangeListener mPageChangeListener; private TabLayout.TabLayoutOnPageChangeListener mPageChangeListener;
...@@ -67,20 +62,11 @@ class KeyboardAccessoryView extends LinearLayout { ...@@ -67,20 +62,11 @@ class KeyboardAccessoryView extends LinearLayout {
mTabLayout = findViewById(R.id.tabs); mTabLayout = findViewById(R.id.tabs);
mSuggestionsView = findViewById(R.id.suggestions_view);
// Apply RTL layout changes to the views children: // Apply RTL layout changes to the views children:
ApiCompatibilityUtils.setLayoutDirection(mSuggestionsView, ApiCompatibilityUtils.setLayoutDirection(mActionsView,
isLayoutRtl() ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); isLayoutRtl() ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
} }
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// When the size changes, the scrolling should be reset.
mSuggestionsView.fullScroll(isLayoutRtl() ? FOCUS_RIGHT : FOCUS_LEFT);
}
void setVisible(boolean visible) { void setVisible(boolean visible) {
if (visible) { if (visible) {
show(); show();
...@@ -145,18 +131,6 @@ class KeyboardAccessoryView extends LinearLayout { ...@@ -145,18 +131,6 @@ class KeyboardAccessoryView extends LinearLayout {
} }
} }
// TODO(crbug/722897): Check to handle RTL.
// TODO(fhorschig): This should use a RecyclerView. The model should contain single suggestions.
/**
* Shows the given suggestions. If set to null, it only removes existing suggestions.
* @param suggestions Autofill suggestion data.
*/
void updateSuggestions(@Nullable AutofillKeyboardSuggestions suggestions) {
mSuggestionsView.removeAllViews();
if (suggestions == null) return;
mSuggestionsView.addView(suggestions);
}
private void show() { private void show() {
bringToFront(); // Needs to overlay every component and the bottom sheet - like a keyboard. bringToFront(); // Needs to overlay every component and the bottom sheet - like a keyboard.
setVisibility(View.VISIBLE); setVisibility(View.VISIBLE);
...@@ -172,15 +146,13 @@ class KeyboardAccessoryView extends LinearLayout { ...@@ -172,15 +146,13 @@ class KeyboardAccessoryView extends LinearLayout {
recyclerView.setLayoutManager( recyclerView.setLayoutManager(
new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false)); new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
int pad = getResources().getDimensionPixelSize(R.dimen.keyboard_accessory_padding);
// Create margins between every element. // Create margins between every element.
recyclerView.addItemDecoration(new HorizontalDividerItemDecoration( recyclerView.addItemDecoration(new HorizontalDividerItemDecoration(pad));
getResources().getDimensionPixelSize(R.dimen.keyboard_accessory_padding)));
// Remove all animations - the accessory shouldn't be visibly built anyway. // Remove all animations - the accessory shouldn't be visibly built anyway.
recyclerView.setItemAnimator(null); recyclerView.setItemAnimator(null);
int pad = getResources().getDimensionPixelSize(R.dimen.keyboard_accessory_padding); recyclerView.setPadding(isLayoutRtl() ? 0 : pad, 0, isLayoutRtl() ? pad : 0, 0);
int halfPad = getResources().getDimensionPixelSize(R.dimen.keyboard_accessory_half_padding);
recyclerView.setPadding(pad, halfPad, pad, halfPad);
} }
} }
...@@ -6,7 +6,9 @@ package org.chromium.chrome.browser.autofill.keyboard_accessory; ...@@ -6,7 +6,9 @@ package org.chromium.chrome.browser.autofill.keyboard_accessory;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView;
import org.chromium.chrome.R; import org.chromium.chrome.R;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Action; import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Action;
...@@ -15,7 +17,6 @@ import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessory ...@@ -15,7 +17,6 @@ import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessory
import org.chromium.chrome.browser.modelutil.LazyViewBinderAdapter; import org.chromium.chrome.browser.modelutil.LazyViewBinderAdapter;
import org.chromium.chrome.browser.modelutil.ListModelChangeProcessor; import org.chromium.chrome.browser.modelutil.ListModelChangeProcessor;
import org.chromium.chrome.browser.modelutil.SimpleListObservable; import org.chromium.chrome.browser.modelutil.SimpleListObservable;
import org.chromium.ui.widget.ButtonCompat;
/** /**
* Observes {@link KeyboardAccessoryModel} changes (like a newly available tab) and triggers the * Observes {@link KeyboardAccessoryModel} changes (like a newly available tab) and triggers the
...@@ -25,24 +26,35 @@ class KeyboardAccessoryViewBinder ...@@ -25,24 +26,35 @@ class KeyboardAccessoryViewBinder
implements LazyViewBinderAdapter.SimpleViewBinder<KeyboardAccessoryModel, implements LazyViewBinderAdapter.SimpleViewBinder<KeyboardAccessoryModel,
KeyboardAccessoryView, PropertyKey> { KeyboardAccessoryView, PropertyKey> {
static class ActionViewHolder extends RecyclerView.ViewHolder { static class ActionViewHolder extends RecyclerView.ViewHolder {
public ActionViewHolder(ButtonCompat actionView) { public ActionViewHolder(View actionView) {
super(actionView); super(actionView);
} }
public static ActionViewHolder create(ViewGroup parent, int viewType) { public static ActionViewHolder create(ViewGroup parent, @AccessoryAction int viewType) {
assert viewType == 0; switch (viewType) {
return new ActionViewHolder( case AccessoryAction.GENERATE_PASSWORD_AUTOMATIC:
(ButtonCompat) LayoutInflater.from(parent.getContext()) return new ActionViewHolder(
.inflate(R.layout.keyboard_accessory_action, parent, false)); LayoutInflater.from(parent.getContext())
.inflate(R.layout.keyboard_accessory_action, parent, false));
case AccessoryAction.AUTOFILL_SUGGESTION:
return new ActionViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.keyboard_accessory_chip, parent, false));
case AccessoryAction.MANAGE_PASSWORDS: // Intentional fallthrough.
case AccessoryAction.COUNT:
assert false : "Type " + viewType + " is not a valid accessory bar action!";
}
assert false : "Action type " + viewType + " was not handled!";
return null;
} }
public void bind(Action action) { public void bind(Action action) {
getActionView().setText(action.getCaption()); getView().setText(action.getCaption());
getActionView().setOnClickListener(view -> action.getCallback().onResult(action)); getView().setOnClickListener(view -> action.getCallback().onResult(action));
} }
private ButtonCompat getActionView() { private TextView getView() {
return (ButtonCompat) super.itemView; return (TextView) super.itemView;
} }
} }
...@@ -126,10 +138,6 @@ class KeyboardAccessoryViewBinder ...@@ -126,10 +138,6 @@ class KeyboardAccessoryViewBinder
view.setTabSelectionAdapter(model.getTabSelectionCallbacks()); view.setTabSelectionAdapter(model.getTabSelectionCallbacks());
return; return;
} }
if (propertyKey == PropertyKey.SUGGESTIONS) {
view.updateSuggestions(model.getAutofillSuggestions());
return;
}
assert false : "Every possible property update needs to be handled!"; assert false : "Every possible property update needs to be handled!";
} }
} }
...@@ -55,6 +55,13 @@ public class ManualFillingCoordinator { ...@@ -55,6 +55,13 @@ public class ManualFillingCoordinator {
return mMediator.handleBackPress(); return mMediator.handleBackPress();
} }
/**
* Ensures that keyboard accessory and keyboard are hidden and reset.
*/
public void dismiss() {
mMediator.dismiss();
}
public void notifyPopupAvailable(DropdownPopupWindow popup) { public void notifyPopupAvailable(DropdownPopupWindow popup) {
mMediator.notifyPopupOpened(popup); mMediator.notifyPopupOpened(popup);
} }
...@@ -74,7 +81,8 @@ public class ManualFillingCoordinator { ...@@ -74,7 +81,8 @@ public class ManualFillingCoordinator {
mMediator.onOpenKeyboard(); mMediator.onOpenKeyboard();
} }
void registerActionProvider(Provider<KeyboardAccessoryData.Action> actionProvider) { void registerActionProvider(
KeyboardAccessoryData.PropertyProvider<KeyboardAccessoryData.Action> actionProvider) {
mMediator.registerActionProvider(actionProvider); mMediator.registerActionProvider(actionProvider);
} }
......
...@@ -11,6 +11,7 @@ import org.chromium.base.VisibleForTesting; ...@@ -11,6 +11,7 @@ import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.ChromeActivity; import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeFeatureList; import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.ChromeTabbedActivity; import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Action;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Provider; import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Provider;
import org.chromium.chrome.browser.compositor.layouts.Layout; import org.chromium.chrome.browser.compositor.layouts.Layout;
import org.chromium.chrome.browser.compositor.layouts.SceneChangeObserver; import org.chromium.chrome.browser.compositor.layouts.SceneChangeObserver;
...@@ -36,13 +37,11 @@ class ManualFillingMediator ...@@ -36,13 +37,11 @@ class ManualFillingMediator
/** /**
* Provides a cache for a given Provider which can repeat the last notification to all * Provides a cache for a given Provider which can repeat the last notification to all
* observers. * observers.
* @param <T> The data that is sent to observers.
*/ */
@VisibleForTesting private class ActionProviderCacheAdapter extends KeyboardAccessoryData.PropertyProvider<Action>
protected class ProviderCacheAdapter<T> extends KeyboardAccessoryData.PropertyProvider<T> implements KeyboardAccessoryData.Observer<Action> {
implements KeyboardAccessoryData.Observer<T> {
private final Tab mTab; private final Tab mTab;
private T[] mLastItems; private Action[] mLastItems;
/** /**
* Creates an adapter that listens to the given |provider| and stores items provided by it. * Creates an adapter that listens to the given |provider| and stores items provided by it.
...@@ -51,7 +50,9 @@ class ManualFillingMediator ...@@ -51,7 +50,9 @@ class ManualFillingMediator
* @param provider The {@link Provider} to observe and whose data to cache. * @param provider The {@link Provider} to observe and whose data to cache.
* @param defaultItems The items to be notified about if the Provider hasn't provided any. * @param defaultItems The items to be notified about if the Provider hasn't provided any.
*/ */
ProviderCacheAdapter(Tab tab, Provider<T> provider, T[] defaultItems) { ActionProviderCacheAdapter(Tab tab, KeyboardAccessoryData.PropertyProvider<Action> provider,
Action[] defaultItems) {
super(provider.mType);
mTab = tab; mTab = tab;
provider.addObserver(this); provider.addObserver(this);
mLastItems = defaultItems; mLastItems = defaultItems;
...@@ -66,10 +67,10 @@ class ManualFillingMediator ...@@ -66,10 +67,10 @@ class ManualFillingMediator
} }
@Override @Override
public void onItemsAvailable(T[] items) { public void onItemsAvailable(int typeId, Action[] actions) {
mLastItems = items; mLastItems = actions;
// Update the contents immediately, if the adapter connects to an active element. // Update the contents immediately, if the adapter connects to an active element.
if (mTab == mActiveBrowserTab) notifyObservers(items); if (mTab == mActiveBrowserTab) notifyObservers(actions);
} }
} }
...@@ -80,7 +81,7 @@ class ManualFillingMediator ...@@ -80,7 +81,7 @@ class ManualFillingMediator
@VisibleForTesting @VisibleForTesting
static class AccessoryState { static class AccessoryState {
@Nullable @Nullable
ProviderCacheAdapter<KeyboardAccessoryData.Action> mActionsProvider; ActionProviderCacheAdapter mActionsProvider;
@Nullable @Nullable
PasswordAccessorySheetCoordinator mPasswordAccessorySheet; PasswordAccessorySheetCoordinator mPasswordAccessorySheet;
} }
...@@ -160,9 +161,9 @@ class ManualFillingMediator ...@@ -160,9 +161,9 @@ class ManualFillingMediator
getPasswordAccessorySheet().registerItemProvider(itemProvider); getPasswordAccessorySheet().registerItemProvider(itemProvider);
} }
void registerActionProvider(Provider<KeyboardAccessoryData.Action> actionProvider) { void registerActionProvider(KeyboardAccessoryData.PropertyProvider<Action> actionProvider) {
ProviderCacheAdapter<KeyboardAccessoryData.Action> adapter = new ProviderCacheAdapter<>( ActionProviderCacheAdapter adapter =
mActiveBrowserTab, actionProvider, new KeyboardAccessoryData.Action[0]); new ActionProviderCacheAdapter(mActiveBrowserTab, actionProvider, new Action[0]);
mModel.get(mActiveBrowserTab).mActionsProvider = adapter; mModel.get(mActiveBrowserTab).mActionsProvider = adapter;
getKeyboardAccessory().registerActionListProvider(adapter); getKeyboardAccessory().registerActionListProvider(adapter);
} }
...@@ -180,6 +181,11 @@ class ManualFillingMediator ...@@ -180,6 +181,11 @@ class ManualFillingMediator
return false; return false;
} }
void dismiss() {
mKeyboardAccessory.dismiss();
UiUtils.hideKeyboard(mActivity.getCurrentFocus());
}
void notifyPopupOpened(DropdownPopupWindow popup) { void notifyPopupOpened(DropdownPopupWindow popup) {
mPopup = popup; mPopup = popup;
} }
......
...@@ -18,7 +18,8 @@ class PasswordAccessoryBridge { ...@@ -18,7 +18,8 @@ class PasswordAccessoryBridge {
private final KeyboardAccessoryData.PropertyProvider<Item> mItemProvider = private final KeyboardAccessoryData.PropertyProvider<Item> mItemProvider =
new KeyboardAccessoryData.PropertyProvider<>(); new KeyboardAccessoryData.PropertyProvider<>();
private final KeyboardAccessoryData.PropertyProvider<Action> mActionProvider = private final KeyboardAccessoryData.PropertyProvider<Action> mActionProvider =
new KeyboardAccessoryData.PropertyProvider<>(); new KeyboardAccessoryData.PropertyProvider<>(
AccessoryAction.GENERATE_PASSWORD_AUTOMATIC);
private final ManualFillingCoordinator mManualFillingCoordinator; private final ManualFillingCoordinator mManualFillingCoordinator;
private final ChromeActivity mActivity; private final ChromeActivity mActivity;
private long mNativeView; private long mNativeView;
......
...@@ -27,7 +27,7 @@ import org.chromium.chrome.browser.modelutil.SimpleRecyclerViewMcp; ...@@ -27,7 +27,7 @@ import org.chromium.chrome.browser.modelutil.SimpleRecyclerViewMcp;
public class PasswordAccessorySheetCoordinator implements KeyboardAccessoryData.Tab.Listener { public class PasswordAccessorySheetCoordinator implements KeyboardAccessoryData.Tab.Listener {
private final Context mContext; private final Context mContext;
private final SimpleListObservable<Item> mModel = new SimpleListObservable<>(); private final SimpleListObservable<Item> mModel = new SimpleListObservable<>();
private final KeyboardAccessoryData.Observer<Item> mMediator = mModel::set; private final KeyboardAccessoryData.Observer<Item> mMediator = (t, items) -> mModel.set(items);
private final KeyboardAccessoryData.Tab mTab; private final KeyboardAccessoryData.Tab mTab;
......
...@@ -89,7 +89,6 @@ chrome_java_sources = [ ...@@ -89,7 +89,6 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/appmenu/AppMenuObserver.java", "java/src/org/chromium/chrome/browser/appmenu/AppMenuObserver.java",
"java/src/org/chromium/chrome/browser/appmenu/AppMenuPropertiesDelegate.java", "java/src/org/chromium/chrome/browser/appmenu/AppMenuPropertiesDelegate.java",
"java/src/org/chromium/chrome/browser/autofill/AutofillKeyboardAccessoryBridge.java", "java/src/org/chromium/chrome/browser/autofill/AutofillKeyboardAccessoryBridge.java",
"java/src/org/chromium/chrome/browser/autofill/AutofillKeyboardSuggestions.java",
"java/src/org/chromium/chrome/browser/autofill/AutofillLogger.java", "java/src/org/chromium/chrome/browser/autofill/AutofillLogger.java",
"java/src/org/chromium/chrome/browser/autofill/AutofillPopupBridge.java", "java/src/org/chromium/chrome/browser/autofill/AutofillPopupBridge.java",
"java/src/org/chromium/chrome/browser/autofill/CardUnmaskBridge.java", "java/src/org/chromium/chrome/browser/autofill/CardUnmaskBridge.java",
......
...@@ -4,18 +4,12 @@ ...@@ -4,18 +4,12 @@
package org.chromium.chrome.browser.autofill; package org.chromium.chrome.browser.autofill;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.scrollTo;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.chromium.ui.base.LocalizationUtils.setRtlForTesting; import static org.chromium.ui.base.LocalizationUtils.setRtlForTesting;
import android.os.Build;
import android.support.test.filters.MediumTest; import android.support.test.filters.MediumTest;
import android.support.v7.widget.RecyclerView;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import org.junit.Assert; import org.junit.Assert;
...@@ -27,7 +21,6 @@ import org.chromium.base.ThreadUtils; ...@@ -27,7 +21,6 @@ import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags; import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.DisabledTest; import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature; import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.base.test.util.RetryOnFailure; import org.chromium.base.test.util.RetryOnFailure;
import org.chromium.base.test.util.UrlUtils; import org.chromium.base.test.util.UrlUtils;
import org.chromium.chrome.R; import org.chromium.chrome.R;
...@@ -89,13 +82,13 @@ public class AutofillKeyboardAccessoryIntegrationTest { ...@@ -89,13 +82,13 @@ public class AutofillKeyboardAccessoryIntegrationTest {
+ "</form></body></html>")); + "</form></body></html>"));
new AutofillTestHelper().setProfile(new AutofillProfile("", "https://www.example.com", new AutofillTestHelper().setProfile(new AutofillProfile("", "https://www.example.com",
"Johnathan Smithonian-Jackson", "Acme Inc", "1 Main\nApt A", "CA", "San Francisco", "Johnathan Smithonian-Jackson", "Acme Inc", "1 Main\nApt A", "CA", "San Francisco",
"", "94102", "", "US", "(415) 888-9999", "john@acme.inc", "en")); "", "94102", "", "US", "(415) 888-9999", "john.sj@acme-mail.inc", "en"));
new AutofillTestHelper().setProfile(new AutofillProfile("", "https://www.example.com", new AutofillTestHelper().setProfile(new AutofillProfile("", "https://www.example.com",
"Jane Erika Donovanova", "Acme Inc", "1 Main\nApt A", "CA", "San Francisco", "", "Jane Erika Donovanova", "Acme Inc", "1 Main\nApt A", "CA", "San Francisco", "",
"94102", "", "US", "(415) 999-0000", "jane@acme.inc", "en")); "94102", "", "US", "(415) 999-0000", "donovanova.j@acme-mail.inc", "en"));
new AutofillTestHelper().setProfile(new AutofillProfile("", "https://www.example.com", new AutofillTestHelper().setProfile(new AutofillProfile("", "https://www.example.com",
"Marcus McSpartangregor", "Acme Inc", "1 Main\nApt A", "CA", "San Francisco", "", "Marcus McSpartangregor", "Acme Inc", "1 Main\nApt A", "CA", "San Francisco", "",
"94102", "", "US", "(415) 999-0000", "marc@acme.inc", "en")); "94102", "", "US", "(415) 999-0000", "marc@acme-mail.inc", "en"));
setRtlForTesting(isRtl); setRtlForTesting(isRtl);
ThreadUtils.runOnUiThreadBlocking(() -> { ThreadUtils.runOnUiThreadBlocking(() -> {
Tab tab = mActivityTestRule.getActivity().getActivityTab(); Tab tab = mActivityTestRule.getActivity().getActivityTab();
...@@ -150,51 +143,19 @@ public class AutofillKeyboardAccessoryIntegrationTest { ...@@ -150,51 +143,19 @@ public class AutofillKeyboardAccessoryIntegrationTest {
public void testSwitchFieldsRescrollsKeyboardAccessory() public void testSwitchFieldsRescrollsKeyboardAccessory()
throws ExecutionException, InterruptedException, TimeoutException { throws ExecutionException, InterruptedException, TimeoutException {
loadTestPage(false); loadTestPage(false);
DOMUtils.clickNode(mWebContentsRef.get(), "fn"); DOMUtils.clickNode(mWebContentsRef.get(), "em");
CriteriaHelper.pollUiThread(Criteria.equals(true, CriteriaHelper.pollUiThread(Criteria.equals(true,
() ()
-> UiUtils.isKeyboardShowing( -> UiUtils.isKeyboardShowing(
mActivityTestRule.getActivity(), mContainerRef.get()))); mActivityTestRule.getActivity(), mContainerRef.get())));
ThreadUtils.runOnUiThreadBlocking(() -> getSuggestionsComponent().scrollTo(2000, 0)); ThreadUtils.runOnUiThreadBlocking(() -> getSuggestionsComponent().scrollToPosition(2));
assertSuggestionScrollPosition(
false, "First suggestion should be off the screen after manual scroll.");
DOMUtils.clickNode(mWebContentsRef.get(), "ln");
assertSuggestionScrollPosition(
true, "First suggestion should be on the screen after switching fields.");
}
/**
* Switching fields in RTL should re-scroll the keyboard accessory to the right.
*
* RTL is only supported on Jelly Bean MR 1+.
* http://android-developers.blogspot.com/2013/03/native-rtl-support-in-android-42.html
*/
@Test
@MediumTest
@Feature({"keyboard-accessory"})
@MinAndroidSdkLevel(Build.VERSION_CODES.JELLY_BEAN_MR1)
@DisabledTest(message = "crbug.com/836027")
public void testSwitchFieldsRescrollsKeyboardAccessoryRtl()
throws ExecutionException, InterruptedException, TimeoutException {
loadTestPage(true);
DOMUtils.clickNode(mWebContentsRef.get(), "fn");
CriteriaHelper.pollUiThread(Criteria.equals(true,
()
-> UiUtils.isKeyboardShowing(
mActivityTestRule.getActivity(), mContainerRef.get())));
assertSuggestionScrollPosition(false, "Last suggestion should be off the screen intially.");
ThreadUtils.runOnUiThreadBlocking(() -> getSuggestionsComponent().scrollTo(-500, 0)); assertSuggestionsScrollState(false, "Should keep the manual scroll position.");
assertSuggestionScrollPosition(
true, "Last suggestion should be on the screen after manual scroll.");
DOMUtils.clickNode(mWebContentsRef.get(), "ln"); DOMUtils.clickNode(mWebContentsRef.get(), "ln");
assertSuggestionScrollPosition( assertSuggestionsScrollState(true, "Should be scrolled back to position 0.");
false, "Last suggestion should be off the screen after switching fields.");
} }
/** /**
...@@ -216,7 +177,7 @@ public class AutofillKeyboardAccessoryIntegrationTest { ...@@ -216,7 +177,7 @@ public class AutofillKeyboardAccessoryIntegrationTest {
mActivityTestRule.getActivity(), mContainerRef.get()))); mActivityTestRule.getActivity(), mContainerRef.get())));
Assert.assertTrue("Keyboard accessory should be visible.", isAccessoryVisible()); Assert.assertTrue("Keyboard accessory should be visible.", isAccessoryVisible());
onView(withText("Marcus")).perform(scrollTo(), click()); ThreadUtils.runOnUiThreadBlocking(() -> getSuggestionAt(0).performClick());
CriteriaHelper.pollUiThread(Criteria.equals(false, CriteriaHelper.pollUiThread(Criteria.equals(false,
() ()
...@@ -225,35 +186,33 @@ public class AutofillKeyboardAccessoryIntegrationTest { ...@@ -225,35 +186,33 @@ public class AutofillKeyboardAccessoryIntegrationTest {
Assert.assertTrue("Keyboard accessory should be hidden.", isAccessoryGone()); Assert.assertTrue("Keyboard accessory should be hidden.", isAccessoryGone());
} }
private void assertSuggestionScrollPosition(boolean shouldBeOnScreen, String failureReason) { private void assertSuggestionsScrollState(boolean isScrollingReset, String failureReason) {
CriteriaHelper.pollUiThread(new Criteria(failureReason) { CriteriaHelper.pollUiThread(new Criteria(failureReason) {
@Override @Override
public boolean isSatisfied() { public boolean isSatisfied() {
View suggestion = getSuggestionAt(0); return isScrollingReset
if (suggestion == null) return false; ? getSuggestionsComponent().computeHorizontalScrollOffset() <= 0
int[] location = new int[2]; : getSuggestionsComponent().computeHorizontalScrollOffset() > 0;
suggestion.getLocationOnScreen(location);
return shouldBeOnScreen ? location[0] > 0 : location[0] < 0;
} }
}); });
} }
private HorizontalScrollView getSuggestionsComponent() { private RecyclerView getSuggestionsComponent() {
final ViewGroup keyboardAccessory = ThreadUtils.runOnUiThreadBlockingNoException( final ViewGroup keyboardAccessory = ThreadUtils.runOnUiThreadBlockingNoException(
() -> mActivityTestRule.getActivity().findViewById(R.id.keyboard_accessory)); () -> mActivityTestRule.getActivity().findViewById(R.id.keyboard_accessory));
if (keyboardAccessory == null) return null; // It might still be loading, so don't assert! if (keyboardAccessory == null) return null; // It might still be loading, so don't assert!
final View scrollview = keyboardAccessory.findViewById(R.id.suggestions_view); final View recyclerView = keyboardAccessory.findViewById(R.id.actions_view);
if (scrollview == null) return null; // It might still be loading, so don't assert! if (recyclerView == null) return null; // It might still be loading, so don't assert!
return (HorizontalScrollView) scrollview; return (RecyclerView) recyclerView;
} }
private View getSuggestionAt(int index) { private View getSuggestionAt(int index) {
ViewGroup scrollview = getSuggestionsComponent(); ViewGroup recyclerView = getSuggestionsComponent();
if (scrollview == null) return null; // It might still be loading, so don't assert! if (recyclerView == null) return null; // It might still be loading, so don't assert!
return scrollview.getChildAt(index); return recyclerView.getChildAt(index);
} }
private boolean isAccessoryVisible() throws ExecutionException { private boolean isAccessoryVisible() throws ExecutionException {
......
...@@ -20,6 +20,7 @@ import static org.hamcrest.Matchers.instanceOf; ...@@ -20,6 +20,7 @@ import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.core.AllOf.allOf; import static org.hamcrest.core.AllOf.allOf;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.chromium.chrome.browser.autofill.keyboard_accessory.AccessoryAction.AUTOFILL_SUGGESTION;
import static org.chromium.chrome.browser.autofill.keyboard_accessory.AccessoryAction.GENERATE_PASSWORD_AUTOMATIC; import static org.chromium.chrome.browser.autofill.keyboard_accessory.AccessoryAction.GENERATE_PASSWORD_AUTOMATIC;
import static org.chromium.chrome.test.util.ViewUtils.VIEW_GONE; import static org.chromium.chrome.test.util.ViewUtils.VIEW_GONE;
import static org.chromium.chrome.test.util.ViewUtils.VIEW_INVISIBLE; import static org.chromium.chrome.test.util.ViewUtils.VIEW_INVISIBLE;
...@@ -138,8 +139,7 @@ public class KeyboardAccessoryViewTest { ...@@ -138,8 +139,7 @@ public class KeyboardAccessoryViewTest {
mModel.getActionList().set(new KeyboardAccessoryData.Action[] { mModel.getActionList().set(new KeyboardAccessoryData.Action[] {
new KeyboardAccessoryData.Action( new KeyboardAccessoryData.Action(
"First", GENERATE_PASSWORD_AUTOMATIC, action -> {}), "First", GENERATE_PASSWORD_AUTOMATIC, action -> {}),
new KeyboardAccessoryData.Action( new KeyboardAccessoryData.Action("Second", AUTOFILL_SUGGESTION, action -> {})});
"Second", GENERATE_PASSWORD_AUTOMATIC, action -> {})});
}); });
onView(isRoot()).check((root, e) -> waitForView((ViewGroup) root, withText("First"))); onView(isRoot()).check((root, e) -> waitForView((ViewGroup) root, withText("First")));
......
...@@ -27,11 +27,13 @@ import org.junit.runner.RunWith; ...@@ -27,11 +27,13 @@ import org.junit.runner.RunWith;
import org.chromium.base.ThreadUtils; import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags; import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.R; import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.ChromeSwitches; import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.autofill.AutofillTestHelper; import org.chromium.chrome.browser.autofill.AutofillTestHelper;
import org.chromium.chrome.browser.autofill.PersonalDataManager; import org.chromium.chrome.browser.autofill.PersonalDataManager;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner; import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule; import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.browser.Features;
import org.chromium.ui.DropdownPopupWindowInterface; import org.chromium.ui.DropdownPopupWindowInterface;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
...@@ -174,6 +176,7 @@ public class ManualFillingIntegrationTest { ...@@ -174,6 +176,7 @@ public class ManualFillingIntegrationTest {
@Test @Test
@SmallTest @SmallTest
@Features.DisableFeatures({ChromeFeatureList.AUTOFILL_KEYBOARD_ACCESSORY})
public void testOpeningSheetDismissesAutofill() public void testOpeningSheetDismissesAutofill()
throws InterruptedException, TimeoutException, ExecutionException { throws InterruptedException, TimeoutException, ExecutionException {
mHelper.loadTestPage(false); mHelper.loadTestPage(false);
......
...@@ -9,12 +9,14 @@ import static org.hamcrest.CoreMatchers.notNullValue; ...@@ -9,12 +9,14 @@ import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.chromium.chrome.browser.autofill.keyboard_accessory.AccessoryAction.AUTOFILL_SUGGESTION;
import static org.chromium.chrome.browser.autofill.keyboard_accessory.AccessoryAction.GENERATE_PASSWORD_AUTOMATIC;
import android.view.ViewStub; import android.view.ViewStub;
import org.junit.Before; import org.junit.Before;
...@@ -28,7 +30,6 @@ import org.chromium.base.metrics.RecordHistogram; ...@@ -28,7 +30,6 @@ import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.test.ShadowRecordHistogram; import org.chromium.base.metrics.test.ShadowRecordHistogram;
import org.chromium.base.test.BaseRobolectricTestRunner; import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.asynctask.CustomShadowAsyncTask; import org.chromium.base.test.asynctask.CustomShadowAsyncTask;
import org.chromium.chrome.browser.autofill.AutofillKeyboardSuggestions;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Action; import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Action;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.PropertyProvider; import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.PropertyProvider;
import org.chromium.chrome.browser.modelutil.ListObservable; import org.chromium.chrome.browser.modelutil.ListObservable;
...@@ -118,7 +119,8 @@ public class KeyboardAccessoryControllerTest { ...@@ -118,7 +119,8 @@ public class KeyboardAccessoryControllerTest {
@Test @Test
public void testModelNotifiesAboutActionsChangedByProvider() { public void testModelNotifiesAboutActionsChangedByProvider() {
final PropertyProvider<Action> testProvider = new PropertyProvider<>(); final PropertyProvider<Action> testProvider =
new PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC);
final Action testAction = new Action(null, 0, null); final Action testAction = new Action(null, 0, null);
mModel.addActionListObserver(mMockActionListObserver); mModel.addActionListObserver(mMockActionListObserver);
...@@ -165,15 +167,21 @@ public class KeyboardAccessoryControllerTest { ...@@ -165,15 +167,21 @@ public class KeyboardAccessoryControllerTest {
@Test @Test
public void testIsVisibleWithSuggestionsBeforeKeyboardComesUp() { public void testIsVisibleWithSuggestionsBeforeKeyboardComesUp() {
KeyboardAccessoryData.PropertyProvider<Action> autofillSuggestionProvider =
new KeyboardAccessoryData.PropertyProvider<>(AUTOFILL_SUGGESTION);
Action suggestion = new Action("Suggestion", AUTOFILL_SUGGESTION, (a) -> {});
mCoordinator.registerActionListProvider(autofillSuggestionProvider);
// Without suggestions, the accessory should remain invisible - even if the keyboard shows. // Without suggestions, the accessory should remain invisible - even if the keyboard shows.
assertThat(mModel.getAutofillSuggestions(), is(nullValue())); assertThat(mModel.getActionList().size(), is(0));
assertThat(mModel.isVisible(), is(false)); assertThat(mModel.isVisible(), is(false));
mMediator.keyboardVisibilityChanged(true); mMediator.keyboardVisibilityChanged(true);
assertThat(mModel.isVisible(), is(false)); assertThat(mModel.isVisible(), is(false));
mMediator.keyboardVisibilityChanged(false); mMediator.keyboardVisibilityChanged(false);
// Adding suggestions doesn't change the visibility by itself. // Adding suggestions doesn't change the visibility by itself.
mMediator.setSuggestions(mock(AutofillKeyboardSuggestions.class)); autofillSuggestionProvider.notifyObservers(new Action[] {suggestion, suggestion});
assertThat(mModel.getActionList().size(), is(2));
assertThat(mModel.isVisible(), is(false)); assertThat(mModel.isVisible(), is(false));
// But as soon as the keyboard comes up, it should be showing. // But as soon as the keyboard comes up, it should be showing.
...@@ -183,16 +191,22 @@ public class KeyboardAccessoryControllerTest { ...@@ -183,16 +191,22 @@ public class KeyboardAccessoryControllerTest {
@Test @Test
public void testIsVisibleWithSuggestionsAfterKeyboardComesUp() { public void testIsVisibleWithSuggestionsAfterKeyboardComesUp() {
KeyboardAccessoryData.PropertyProvider<Action> autofillSuggestionProvider =
new KeyboardAccessoryData.PropertyProvider<>(AUTOFILL_SUGGESTION);
Action suggestion = new Action("Suggestion", AUTOFILL_SUGGESTION, (a) -> {});
mCoordinator.registerActionListProvider(autofillSuggestionProvider);
// Without any suggestions, the accessory should remain invisible. // Without any suggestions, the accessory should remain invisible.
assertThat(mModel.getAutofillSuggestions(), is(nullValue()));
assertThat(mModel.isVisible(), is(false)); assertThat(mModel.isVisible(), is(false));
assertThat(mModel.getActionList().size(), is(0));
// If the keyboard comes up, but there are no suggestions set, keep the accessory hidden. // If the keyboard comes up, but there are no suggestions set, keep the accessory hidden.
mMediator.keyboardVisibilityChanged(true); mMediator.keyboardVisibilityChanged(true);
assertThat(mModel.isVisible(), is(false)); assertThat(mModel.isVisible(), is(false));
// Adding suggestions while the keyboard is visible triggers the accessory. // Adding suggestions while the keyboard is visible triggers the accessory.
mMediator.setSuggestions(mock(AutofillKeyboardSuggestions.class)); autofillSuggestionProvider.notifyObservers(new Action[] {suggestion, suggestion});
assertThat(mModel.getActionList().size(), is(2));
assertThat(mModel.isVisible(), is(true)); assertThat(mModel.isVisible(), is(true));
} }
...@@ -208,6 +222,57 @@ public class KeyboardAccessoryControllerTest { ...@@ -208,6 +222,57 @@ public class KeyboardAccessoryControllerTest {
assertThat(mModel.isVisible(), is(true)); assertThat(mModel.isVisible(), is(true));
} }
@Test
public void testSortsActionsBasedOnType() {
KeyboardAccessoryData.PropertyProvider<Action> generationProvider =
new KeyboardAccessoryData.PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC);
KeyboardAccessoryData.PropertyProvider<Action> autofillSuggestionProvider =
new KeyboardAccessoryData.PropertyProvider<>(AUTOFILL_SUGGESTION);
mCoordinator.registerActionListProvider(generationProvider);
mCoordinator.registerActionListProvider(autofillSuggestionProvider);
Action suggestion1 = new Action("FirstSuggestion", AUTOFILL_SUGGESTION, (a) -> {});
Action suggestion2 = new Action("SecondSuggestion", AUTOFILL_SUGGESTION, (a) -> {});
Action generationAction = new Action("Generate", GENERATE_PASSWORD_AUTOMATIC, (a) -> {});
autofillSuggestionProvider.notifyObservers(new Action[] {suggestion1, suggestion2});
generationProvider.notifyObservers(new Action[] {generationAction});
// Autofill suggestions should always come last, independent of when they were added.
assertThat(mModel.getActionList().size(), is(3));
assertThat(mModel.getActionList().indexOf(generationAction), is(0));
assertThat(mModel.getActionList().indexOf(suggestion1), is(1));
assertThat(mModel.getActionList().indexOf(suggestion2), is(2));
}
@Test
public void testDeletingActionsAffectsOnlyOneType() {
KeyboardAccessoryData.PropertyProvider<Action> generationProvider =
new KeyboardAccessoryData.PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC);
KeyboardAccessoryData.PropertyProvider<Action> autofillSuggestionProvider =
new KeyboardAccessoryData.PropertyProvider<>(AUTOFILL_SUGGESTION);
mCoordinator.registerActionListProvider(generationProvider);
mCoordinator.registerActionListProvider(autofillSuggestionProvider);
Action suggestion = new Action("NewSuggestion", AUTOFILL_SUGGESTION, (a) -> {});
Action generationAction = new Action("Generate", GENERATE_PASSWORD_AUTOMATIC, (a) -> {});
autofillSuggestionProvider.notifyObservers(new Action[] {suggestion, suggestion});
generationProvider.notifyObservers(new Action[] {generationAction});
assertThat(mModel.getActionList().size(), is(3));
// Drop all Autofill suggestions. Only the generation action should remain.
autofillSuggestionProvider.notifyObservers(new Action[0]);
assertThat(mModel.getActionList().size(), is(1));
assertThat(mModel.getActionList().indexOf(generationAction), is(0));
// Readd an Autofill suggestion and drop the generation. Only the suggestion should remain.
autofillSuggestionProvider.notifyObservers(new Action[] {suggestion});
generationProvider.notifyObservers(new Action[0]);
assertThat(mModel.getActionList().size(), is(1));
assertThat(mModel.getActionList().indexOf(suggestion), is(0));
}
@Test @Test
public void testActionsRemovedWhenNotVisible() { public void testActionsRemovedWhenNotVisible() {
// Make the accessory visible and add an action to it. // Make the accessory visible and add an action to it.
...@@ -278,7 +343,11 @@ public class KeyboardAccessoryControllerTest { ...@@ -278,7 +343,11 @@ public class KeyboardAccessoryControllerTest {
// Adding suggestions adds to the suggestions bucket - and again to tabs and total. // Adding suggestions adds to the suggestions bucket - and again to tabs and total.
mMediator.keyboardVisibilityChanged(false); // Hide, so it's brought up again. mMediator.keyboardVisibilityChanged(false); // Hide, so it's brought up again.
mMediator.setSuggestions(mock(AutofillKeyboardSuggestions.class)); KeyboardAccessoryData.PropertyProvider<Action> autofillSuggestionProvider =
new KeyboardAccessoryData.PropertyProvider<>(AUTOFILL_SUGGESTION);
Action suggestion = new Action("Suggestion", AUTOFILL_SUGGESTION, (a) -> {});
mCoordinator.registerActionListProvider(autofillSuggestionProvider);
autofillSuggestionProvider.notifyObservers(new Action[] {suggestion});
mMediator.keyboardVisibilityChanged(true); mMediator.keyboardVisibilityChanged(true);
// Hiding the keyboard clears actions, so don't log more actions from here on out. // Hiding the keyboard clears actions, so don't log more actions from here on out.
...@@ -289,7 +358,7 @@ public class KeyboardAccessoryControllerTest { ...@@ -289,7 +358,7 @@ public class KeyboardAccessoryControllerTest {
// Removing suggestions adds to everything but the suggestions bucket. The value remains. // Removing suggestions adds to everything but the suggestions bucket. The value remains.
mMediator.keyboardVisibilityChanged(false); // Hide, so it's brought up again. mMediator.keyboardVisibilityChanged(false); // Hide, so it's brought up again.
mMediator.setSuggestions(null); autofillSuggestionProvider.notifyObservers(new Action[0]);
mMediator.keyboardVisibilityChanged(true); mMediator.keyboardVisibilityChanged(true);
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_AUTOFILL_SUGGESTIONS), is(1)); assertThat(getShownMetricsCount(AccessoryBarContents.WITH_AUTOFILL_SUGGESTIONS), is(1));
...@@ -312,8 +381,12 @@ public class KeyboardAccessoryControllerTest { ...@@ -312,8 +381,12 @@ public class KeyboardAccessoryControllerTest {
assertThat(getShownMetricsCount(AccessoryBarContents.ANY_CONTENTS), is(1)); assertThat(getShownMetricsCount(AccessoryBarContents.ANY_CONTENTS), is(1));
// Adding a tabs or suggestions now doesn't change the impression count // Adding a tabs or suggestions now doesn't change the impression count
KeyboardAccessoryData.PropertyProvider<Action> autofillSuggestionProvider =
new KeyboardAccessoryData.PropertyProvider<>(AUTOFILL_SUGGESTION);
Action suggestion = new Action("Suggestion", AUTOFILL_SUGGESTION, (a) -> {});
mCoordinator.registerActionListProvider(autofillSuggestionProvider);
autofillSuggestionProvider.notifyObservers(new Action[] {suggestion});
mCoordinator.addTab(mTestTab); mCoordinator.addTab(mTestTab);
mMediator.setSuggestions(mock(AutofillKeyboardSuggestions.class));
mMediator.keyboardVisibilityChanged(true); mMediator.keyboardVisibilityChanged(true);
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_TABS), is(0)); assertThat(getShownMetricsCount(AccessoryBarContents.WITH_TABS), is(0));
......
...@@ -177,8 +177,10 @@ public class ManualFillingControllerTest { ...@@ -177,8 +177,10 @@ public class ManualFillingControllerTest {
@Test @Test
public void testKeyboardAccessoryActionsPersistAfterSwitchingBrowserTabs() { public void testKeyboardAccessoryActionsPersistAfterSwitchingBrowserTabs() {
ManualFillingMediator mediator = mController.getMediatorForTesting(); ManualFillingMediator mediator = mController.getMediatorForTesting();
Provider<Action> firstTabProvider = new PropertyProvider<>(); PropertyProvider<Action> firstTabProvider =
Provider<Action> secondTabProvider = new PropertyProvider<>(); new PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC);
PropertyProvider<Action> secondTabProvider =
new PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC);
SimpleListObservable<Action> keyboardActions = mediator.getKeyboardAccessory() SimpleListObservable<Action> keyboardActions = mediator.getKeyboardAccessory()
.getMediatorForTesting() .getMediatorForTesting()
.getModelForTesting() .getModelForTesting()
...@@ -196,7 +198,7 @@ public class ManualFillingControllerTest { ...@@ -196,7 +198,7 @@ public class ManualFillingControllerTest {
// Simulate creating a second tab: // Simulate creating a second tab:
Tab secondTab = addTab(mediator, 2222, firstTab); Tab secondTab = addTab(mediator, 2222, firstTab);
mController.registerActionProvider(secondTabProvider); mController.registerActionProvider(secondTabProvider);
secondTabProvider.notifyObservers(new Action[] {}); secondTabProvider.notifyObservers(new Action[0]);
mMockItemListObserver.onItemRangeRemoved(keyboardActions, 0, 1); mMockItemListObserver.onItemRangeRemoved(keyboardActions, 0, 1);
assertThat(keyboardActions.size(), is(0)); // No actions on this tab. assertThat(keyboardActions.size(), is(0)); // No actions on this tab.
...@@ -270,12 +272,14 @@ public class ManualFillingControllerTest { ...@@ -270,12 +272,14 @@ public class ManualFillingControllerTest {
// Open a tab. // Open a tab.
Tab tab = addTab(mediator, 1111, null); Tab tab = addTab(mediator, 1111, null);
// Add an action provider that never provided actions. // Add an action provider that never provided actions.
mController.registerActionProvider(new PropertyProvider<>()); mController.registerActionProvider(
new PropertyProvider<Action>(GENERATE_PASSWORD_AUTOMATIC));
assertThat(keyboardAccessoryModel.getActionList().size(), is(0)); assertThat(keyboardAccessoryModel.getActionList().size(), is(0));
// Create a new tab with an action: // Create a new tab with an action:
Tab secondTab = addTab(mediator, 1111, tab); Tab secondTab = addTab(mediator, 1111, tab);
PropertyProvider<Action> provider = new PropertyProvider<>(); PropertyProvider<Action> provider =
new PropertyProvider<Action>(GENERATE_PASSWORD_AUTOMATIC);
mController.registerActionProvider(provider); mController.registerActionProvider(provider);
provider.notifyObservers(new Action[] { provider.notifyObservers(new Action[] {
new Action("Test Action", GENERATE_PASSWORD_AUTOMATIC, (action) -> {})}); new Action("Test Action", GENERATE_PASSWORD_AUTOMATIC, (action) -> {})});
...@@ -294,13 +298,15 @@ public class ManualFillingControllerTest { ...@@ -294,13 +298,15 @@ public class ManualFillingControllerTest {
// Open a tab. // Open a tab.
Tab tab = addTab(mediator, 1111, null); Tab tab = addTab(mediator, 1111, null);
// Add an action provider that hasn't provided actions yet. // Add an action provider that hasn't provided actions yet.
PropertyProvider<Action> delayedProvider = new PropertyProvider<>(); PropertyProvider<Action> delayedProvider =
new PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC);
mController.registerActionProvider(delayedProvider); mController.registerActionProvider(delayedProvider);
assertThat(keyboardAccessoryModel.getActionList().size(), is(0)); assertThat(keyboardAccessoryModel.getActionList().size(), is(0));
// Create and switch to a new tab: // Create and switch to a new tab:
Tab secondTab = addTab(mediator, 1111, tab); Tab secondTab = addTab(mediator, 1111, tab);
PropertyProvider<Action> provider = new PropertyProvider<>(); PropertyProvider<Action> provider =
new PropertyProvider<Action>(GENERATE_PASSWORD_AUTOMATIC);
mController.registerActionProvider(provider); mController.registerActionProvider(provider);
// And provide data to the active tab. // And provide data to the active tab.
...@@ -331,9 +337,11 @@ public class ManualFillingControllerTest { ...@@ -331,9 +337,11 @@ public class ManualFillingControllerTest {
.getModelForTesting(); .getModelForTesting();
Provider<Item> firstTabProvider = new PropertyProvider<>(); Provider<Item> firstTabProvider = new PropertyProvider<>();
PropertyProvider<Action> firstActionProvider = new PropertyProvider<>(); PropertyProvider<Action> firstActionProvider =
new PropertyProvider<Action>(GENERATE_PASSWORD_AUTOMATIC);
Provider<Item> secondTabProvider = new PropertyProvider<>(); Provider<Item> secondTabProvider = new PropertyProvider<>();
PropertyProvider<Action> secondActionProvider = new PropertyProvider<>(); PropertyProvider<Action> secondActionProvider =
new PropertyProvider<Action>(GENERATE_PASSWORD_AUTOMATIC);
// Simulate opening a new tab: // Simulate opening a new tab:
Tab firstTab = addTab(mediator, 1111, null); Tab firstTab = addTab(mediator, 1111, null);
......
...@@ -36,6 +36,9 @@ enum class AccessorySheetTrigger { ...@@ -36,6 +36,9 @@ enum class AccessorySheetTrigger {
}; };
// Used to record metrics specific to a tab types (e.g. passwords, payments). // Used to record metrics specific to a tab types (e.g. passwords, payments).
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused. Must be kept in sync with the enum
// in enums.xml. A java IntDef@ is generated from this.
// GENERATED_JAVA_ENUM_PACKAGE: ( // GENERATED_JAVA_ENUM_PACKAGE: (
// org.chromium.chrome.browser.autofill.keyboard_accessory) // org.chromium.chrome.browser.autofill.keyboard_accessory)
enum class AccessoryTabType { enum class AccessoryTabType {
...@@ -45,15 +48,22 @@ enum class AccessoryTabType { ...@@ -45,15 +48,22 @@ enum class AccessoryTabType {
}; };
// Used to record impressions and clicks on specific actions and links. // Used to record impressions and clicks on specific actions and links.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused. Must be kept in sync with the enum
// in enums.xml. A java IntDef@ is generated from this.
// GENERATED_JAVA_ENUM_PACKAGE: ( // GENERATED_JAVA_ENUM_PACKAGE: (
// org.chromium.chrome.browser.autofill.keyboard_accessory) // org.chromium.chrome.browser.autofill.keyboard_accessory)
enum class AccessoryAction { enum class AccessoryAction {
GENERATE_PASSWORD_AUTOMATIC = 0, GENERATE_PASSWORD_AUTOMATIC = 0,
MANAGE_PASSWORDS = 1, MANAGE_PASSWORDS = 1,
AUTOFILL_SUGGESTION = 2,
COUNT, COUNT,
}; };
// Used to record which type of suggestion was selected. // Used to record which type of suggestion was selected.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused. Must be kept in sync with the enum
// in enums.xml. A java IntDef@ is generated from this.
// GENERATED_JAVA_ENUM_PACKAGE: ( // GENERATED_JAVA_ENUM_PACKAGE: (
// org.chromium.chrome.browser.autofill.keyboard_accessory) // org.chromium.chrome.browser.autofill.keyboard_accessory)
enum class AccessorySuggestionType { enum class AccessorySuggestionType {
......
...@@ -361,8 +361,9 @@ uploading your change for review. These are checked by presubmit scripts. ...@@ -361,8 +361,9 @@ uploading your change for review. These are checked by presubmit scripts.
</enum> </enum>
<enum name="AccessoryAction"> <enum name="AccessoryAction">
<int value="0" label="Automatic password generation selected"/> <int value="0" label="Automatic password generation"/>
<int value="1" label="'Manage all passwords' link selected"/> <int value="1" label="'Manage all passwords' link"/>
<int value="2" label="Autofill suggestion"/>
</enum> </enum>
<enum name="AccessoryBarContents"> <enum name="AccessoryBarContents">
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