Commit a2ffe5a5 authored by Pavel Yatsuk's avatar Pavel Yatsuk Committed by Chromium LUCI CQ

[Messages] Option to block save password message for a site

This CL introduces a logic to block trigering future Save Password
messages for the current site. This logic is similar to "Never button"
in Save Password infobar.

For M88 Save Password message shows a menu with a single "Never" item.
In the future we will introduce a modal dialog with other options.

BUG=1160393
R=twellington@chromium.org

Change-Id: Idecfb30827b123197a5ad5af06433e6a96dfad2d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2599118
Commit-Queue: Pavel Yatsuk <pavely@chromium.org>
Reviewed-by: default avatarTheresa  <twellington@chromium.org>
Cr-Commit-Position: refs/heads/master@{#839045}
parent a1b49bd8
...@@ -67,7 +67,7 @@ void SavePasswordMessageDelegate::CreateMessage( ...@@ -67,7 +67,7 @@ void SavePasswordMessageDelegate::CreateMessage(
// SavePasswordMessageDelegate owns message_. Callbacks won't be called after // SavePasswordMessageDelegate owns message_. Callbacks won't be called after
// the current object is destroyed. // the current object is destroyed.
message_ = std::make_unique<messages::MessageWrapper>( message_ = std::make_unique<messages::MessageWrapper>(
base::BindOnce(&SavePasswordMessageDelegate::HandleActionClick, base::BindOnce(&SavePasswordMessageDelegate::HandleSaveClick,
base::Unretained(this)), base::Unretained(this)),
base::BindOnce(&SavePasswordMessageDelegate::HandleDismissCallback, base::BindOnce(&SavePasswordMessageDelegate::HandleDismissCallback,
base::Unretained(this))); base::Unretained(this)));
...@@ -95,17 +95,30 @@ void SavePasswordMessageDelegate::CreateMessage( ...@@ -95,17 +95,30 @@ void SavePasswordMessageDelegate::CreateMessage(
l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_SAVE_BUTTON)); l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_SAVE_BUTTON));
message_->SetIconResourceId( message_->SetIconResourceId(
ResourceMapper::MapToJavaDrawableId(IDR_ANDROID_INFOBAR_SAVE_PASSWORD)); ResourceMapper::MapToJavaDrawableId(IDR_ANDROID_INFOBAR_SAVE_PASSWORD));
message_->SetSecondaryIconResourceId(
ResourceMapper::MapToJavaDrawableId(IDR_ANDROID_AUTOFILL_SETTINGS));
message_->SetSecondaryActionText(
l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_BLOCKLIST_BUTTON));
message_->SetSecondaryActionCallback(base::BindOnce(
&SavePasswordMessageDelegate::HandleNeverClick, base::Unretained(this)));
// Recording metrics is not a part of message creation. It is included here to // Recording metrics is not a part of message creation. It is included here to
// ensure metrics recording test coverage. // ensure metrics recording test coverage.
RecordMessageShownMetrics(); RecordMessageShownMetrics();
} }
void SavePasswordMessageDelegate::HandleActionClick() { void SavePasswordMessageDelegate::HandleSaveClick() {
form_to_save_->Save(); form_to_save_->Save();
ui_dismissal_reason_ = password_manager::metrics_util::CLICKED_ACCEPT; ui_dismissal_reason_ = password_manager::metrics_util::CLICKED_ACCEPT;
} }
void SavePasswordMessageDelegate::HandleNeverClick() {
form_to_save_->Blocklist();
ui_dismissal_reason_ = password_manager::metrics_util::CLICKED_NEVER;
DismissSavePasswordPrompt();
}
void SavePasswordMessageDelegate::HandleDismissCallback() { void SavePasswordMessageDelegate::HandleDismissCallback() {
// The message is dismissed. Record metrics and cleanup state. // The message is dismissed. Record metrics and cleanup state.
RecordDismissalReasonMetrics(); RecordDismissalReasonMetrics();
......
...@@ -40,8 +40,9 @@ class SavePasswordMessageDelegate { ...@@ -40,8 +40,9 @@ class SavePasswordMessageDelegate {
std::unique_ptr<password_manager::PasswordFormManagerForUI> form_to_save, std::unique_ptr<password_manager::PasswordFormManagerForUI> form_to_save,
bool is_saving_google_account); bool is_saving_google_account);
// Called in response to user clicking "Save" button. // Called in response to user clicking "Save" and "Never" buttons.
void HandleActionClick(); void HandleSaveClick();
void HandleNeverClick();
// Called when the message is dismissed. // Called when the message is dismissed.
void HandleDismissCallback(); void HandleDismissCallback();
......
...@@ -49,6 +49,7 @@ class SavePasswordMessageDelegateTest : public ChromeRenderViewHostTestHarness { ...@@ -49,6 +49,7 @@ class SavePasswordMessageDelegateTest : public ChromeRenderViewHostTestHarness {
void CreateMessage(std::unique_ptr<PasswordFormManagerForUI> form_to_save, void CreateMessage(std::unique_ptr<PasswordFormManagerForUI> form_to_save,
bool is_saving_google_account); bool is_saving_google_account);
void TriggerActionClick(); void TriggerActionClick();
void TriggerBlocklistClick();
void TriggerMessageDismissedCallback(); void TriggerMessageDismissedCallback();
messages::MessageWrapper* GetMessageWrapper(); messages::MessageWrapper* GetMessageWrapper();
...@@ -109,6 +110,11 @@ void SavePasswordMessageDelegateTest::TriggerActionClick() { ...@@ -109,6 +110,11 @@ void SavePasswordMessageDelegateTest::TriggerActionClick() {
GetMessageWrapper()->HandleActionClick(base::android::AttachCurrentThread()); GetMessageWrapper()->HandleActionClick(base::android::AttachCurrentThread());
} }
void SavePasswordMessageDelegateTest::TriggerBlocklistClick() {
GetMessageWrapper()->HandleSecondaryActionClick(
base::android::AttachCurrentThread());
}
void SavePasswordMessageDelegateTest::TriggerMessageDismissedCallback() { void SavePasswordMessageDelegateTest::TriggerMessageDismissedCallback() {
GetMessageWrapper()->HandleDismissCallback( GetMessageWrapper()->HandleDismissCallback(
base::android::AttachCurrentThread()); base::android::AttachCurrentThread());
...@@ -158,9 +164,13 @@ TEST_F(SavePasswordMessageDelegateTest, MessagePropertyValues) { ...@@ -158,9 +164,13 @@ TEST_F(SavePasswordMessageDelegateTest, MessagePropertyValues) {
EXPECT_EQ(l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_SAVE_BUTTON), EXPECT_EQ(l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_SAVE_BUTTON),
GetMessageWrapper()->GetPrimaryButtonText()); GetMessageWrapper()->GetPrimaryButtonText());
EXPECT_EQ(l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_BLOCKLIST_BUTTON),
GetMessageWrapper()->GetSecondaryActionText());
EXPECT_EQ( EXPECT_EQ(
ResourceMapper::MapToJavaDrawableId(IDR_ANDROID_INFOBAR_SAVE_PASSWORD), ResourceMapper::MapToJavaDrawableId(IDR_ANDROID_INFOBAR_SAVE_PASSWORD),
GetMessageWrapper()->GetIconResourceId()); GetMessageWrapper()->GetIconResourceId());
EXPECT_EQ(ResourceMapper::MapToJavaDrawableId(IDR_ANDROID_AUTOFILL_SETTINGS),
GetMessageWrapper()->GetSecondaryIconResourceId());
TriggerMessageDismissedCallback(); TriggerMessageDismissedCallback();
} }
......
...@@ -7,13 +7,17 @@ package org.chromium.components.browser_ui.widget.listmenu; ...@@ -7,13 +7,17 @@ package org.chromium.components.browser_ui.widget.listmenu;
import org.chromium.ui.modelutil.PropertyKey; import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel.WritableBooleanPropertyKey; import org.chromium.ui.modelutil.PropertyModel.WritableBooleanPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.WritableIntPropertyKey; import org.chromium.ui.modelutil.PropertyModel.WritableIntPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.WritableObjectPropertyKey;
/** /**
* The properties controlling the state of the list menu items. Any given list item can have either * The properties controlling the state of the list menu items. Any given list item can have either
* one start icon or one end icon but not both. * one start icon or one end icon but not both.
*/ */
public class ListMenuItemProperties { public class ListMenuItemProperties {
// TODO(crbug.com/1161388): Consider passing menu item title through TITLE property instead of
// TITLE_ID.
public static final WritableIntPropertyKey TITLE_ID = new WritableIntPropertyKey(); public static final WritableIntPropertyKey TITLE_ID = new WritableIntPropertyKey();
public static final WritableObjectPropertyKey<String> TITLE = new WritableObjectPropertyKey<>();
public static final WritableIntPropertyKey START_ICON_ID = new WritableIntPropertyKey(); public static final WritableIntPropertyKey START_ICON_ID = new WritableIntPropertyKey();
public static final WritableIntPropertyKey END_ICON_ID = new WritableIntPropertyKey(); public static final WritableIntPropertyKey END_ICON_ID = new WritableIntPropertyKey();
public static final WritableIntPropertyKey TINT_COLOR_ID = new WritableIntPropertyKey(); public static final WritableIntPropertyKey TINT_COLOR_ID = new WritableIntPropertyKey();
...@@ -21,5 +25,5 @@ public class ListMenuItemProperties { ...@@ -21,5 +25,5 @@ public class ListMenuItemProperties {
public static final WritableBooleanPropertyKey ENABLED = new WritableBooleanPropertyKey(); public static final WritableBooleanPropertyKey ENABLED = new WritableBooleanPropertyKey();
public static final PropertyKey[] ALL_KEYS = { public static final PropertyKey[] ALL_KEYS = {
TITLE_ID, START_ICON_ID, END_ICON_ID, MENU_ITEM_ID, ENABLED, TINT_COLOR_ID}; TITLE_ID, TITLE, START_ICON_ID, END_ICON_ID, MENU_ITEM_ID, ENABLED, TINT_COLOR_ID};
} }
...@@ -28,6 +28,8 @@ public class ListMenuItemViewBinder { ...@@ -28,6 +28,8 @@ public class ListMenuItemViewBinder {
ImageView endIcon = view.findViewById(R.id.menu_item_end_icon); ImageView endIcon = view.findViewById(R.id.menu_item_end_icon);
if (propertyKey == ListMenuItemProperties.TITLE_ID) { if (propertyKey == ListMenuItemProperties.TITLE_ID) {
textView.setText(model.get(ListMenuItemProperties.TITLE_ID)); textView.setText(model.get(ListMenuItemProperties.TITLE_ID));
} else if (propertyKey == ListMenuItemProperties.TITLE) {
textView.setText(model.get(ListMenuItemProperties.TITLE));
} else if (propertyKey == ListMenuItemProperties.START_ICON_ID } else if (propertyKey == ListMenuItemProperties.START_ICON_ID
|| propertyKey == ListMenuItemProperties.END_ICON_ID) { || propertyKey == ListMenuItemProperties.END_ICON_ID) {
int id = model.get((ReadableIntPropertyKey) propertyKey); int id = model.get((ReadableIntPropertyKey) propertyKey);
......
...@@ -114,6 +114,7 @@ android_library("javatests") { ...@@ -114,6 +114,7 @@ android_library("javatests") {
testonly = true testonly = true
sources = [ sources = [
"java/src/org/chromium/components/messages/MessageBannerRenderTest.java", "java/src/org/chromium/components/messages/MessageBannerRenderTest.java",
"java/src/org/chromium/components/messages/MessageBannerViewTest.java",
"java/src/org/chromium/components/messages/SingleActionMessageTest.java", "java/src/org/chromium/components/messages/SingleActionMessageTest.java",
] ]
resources_package = "org.chromium.components.messages" resources_package = "org.chromium.components.messages"
...@@ -128,6 +129,7 @@ android_library("javatests") { ...@@ -128,6 +129,7 @@ android_library("javatests") {
"//third_party/android_deps:androidx_appcompat_appcompat_resources_java", "//third_party/android_deps:androidx_appcompat_appcompat_resources_java",
"//third_party/android_deps:androidx_core_core_java", "//third_party/android_deps:androidx_core_core_java",
"//third_party/android_deps:androidx_test_runner_java", "//third_party/android_deps:androidx_test_runner_java",
"//third_party/android_deps:espresso_java",
"//third_party/android_support_test_runner:rules_java", "//third_party/android_support_test_runner:rules_java",
"//third_party/android_support_test_runner:runner_java", "//third_party/android_support_test_runner:runner_java",
"//third_party/junit", "//third_party/junit",
......
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
</LinearLayout> </LinearLayout>
<!-- Content description is set programmatically according to secondary button icon. --> <!-- Content description is set programmatically according to secondary button icon. -->
<ImageView <org.chromium.components.browser_ui.widget.listmenu.ListMenuButton
android:id="@+id/message_secondary_button" android:id="@+id/message_secondary_button"
android:tint="@color/default_icon_color_secondary" android:tint="@color/default_icon_color_secondary"
android:layout_weight="0" android:layout_weight="0"
...@@ -63,7 +63,8 @@ ...@@ -63,7 +63,8 @@
android:contentDescription="@null" android:contentDescription="@null"
android:minWidth="@dimen/message_banner_button_min_width" android:minWidth="@dimen/message_banner_button_min_width"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" /> android:layout_height="match_parent"
android:background="@null" />
<ImageView <ImageView
android:id="@+id/message_divider" android:id="@+id/message_divider"
......
...@@ -20,6 +20,8 @@ public class MessageBannerProperties { ...@@ -20,6 +20,8 @@ public class MessageBannerProperties {
new WritableObjectPropertyKey<>(); new WritableObjectPropertyKey<>();
public static final WritableObjectPropertyKey<Runnable> ON_PRIMARY_ACTION = public static final WritableObjectPropertyKey<Runnable> ON_PRIMARY_ACTION =
new WritableObjectPropertyKey<>(); new WritableObjectPropertyKey<>();
public static final WritableObjectPropertyKey<Runnable> ON_SECONDARY_ACTION =
new WritableObjectPropertyKey<>();
public static final WritableObjectPropertyKey<String> TITLE = new WritableObjectPropertyKey<>(); public static final WritableObjectPropertyKey<String> TITLE = new WritableObjectPropertyKey<>();
public static final WritableObjectPropertyKey<String> DESCRIPTION = public static final WritableObjectPropertyKey<String> DESCRIPTION =
new WritableObjectPropertyKey<>(); new WritableObjectPropertyKey<>();
...@@ -29,6 +31,10 @@ public class MessageBannerProperties { ...@@ -29,6 +31,10 @@ public class MessageBannerProperties {
// Secondary icon is shown as a button, so content description should be always set. // Secondary icon is shown as a button, so content description should be always set.
public static final WritableObjectPropertyKey<Drawable> SECONDARY_ICON = public static final WritableObjectPropertyKey<Drawable> SECONDARY_ICON =
new WritableObjectPropertyKey<>(); new WritableObjectPropertyKey<>();
public static final WritableIntPropertyKey SECONDARY_ICON_RESOURCE_ID =
new WritableIntPropertyKey();
public static final WritableObjectPropertyKey<String> SECONDARY_ACTION_TEXT =
new WritableObjectPropertyKey<>();
public static final WritableObjectPropertyKey<String> SECONDARY_ICON_CONTENT_DESCRIPTION = public static final WritableObjectPropertyKey<String> SECONDARY_ICON_CONTENT_DESCRIPTION =
new WritableObjectPropertyKey<>(); new WritableObjectPropertyKey<>();
// TODO(crbug.com/1123947): remove this since on_dismissed is not a property of the view? // TODO(crbug.com/1123947): remove this since on_dismissed is not a property of the view?
...@@ -49,12 +55,13 @@ public class MessageBannerProperties { ...@@ -49,12 +55,13 @@ public class MessageBannerProperties {
// up references. // up references.
public static final PropertyKey[] ALL_KEYS = public static final PropertyKey[] ALL_KEYS =
new PropertyKey[] {PRIMARY_BUTTON_TEXT, PRIMARY_BUTTON_CLICK_LISTENER, TITLE, new PropertyKey[] {PRIMARY_BUTTON_TEXT, PRIMARY_BUTTON_CLICK_LISTENER, TITLE,
DESCRIPTION, ICON, ICON_RESOURCE_ID, SECONDARY_ICON, DESCRIPTION, ICON, ICON_RESOURCE_ID, SECONDARY_ICON, SECONDARY_ICON_RESOURCE_ID,
SECONDARY_ICON_CONTENT_DESCRIPTION, TRANSLATION_Y, ALPHA, ON_TOUCH_RUNNABLE, SECONDARY_ACTION_TEXT, SECONDARY_ICON_CONTENT_DESCRIPTION, TRANSLATION_Y, ALPHA,
ON_PRIMARY_ACTION}; ON_TOUCH_RUNNABLE, ON_PRIMARY_ACTION, ON_SECONDARY_ACTION};
public static final PropertyKey[] SINGLE_ACTION_MESSAGE_KEYS = public static final PropertyKey[] SINGLE_ACTION_MESSAGE_KEYS =
new PropertyKey[] {PRIMARY_BUTTON_TEXT, PRIMARY_BUTTON_CLICK_LISTENER, TITLE, new PropertyKey[] {PRIMARY_BUTTON_TEXT, PRIMARY_BUTTON_CLICK_LISTENER, TITLE,
DESCRIPTION, ICON, ICON_RESOURCE_ID, ON_DISMISSED, TRANSLATION_Y, ALPHA, DESCRIPTION, ICON, ICON_RESOURCE_ID, SECONDARY_ICON, SECONDARY_ICON_RESOURCE_ID,
ON_TOUCH_RUNNABLE, ON_PRIMARY_ACTION}; SECONDARY_ACTION_TEXT, ON_DISMISSED, TRANSLATION_Y, ALPHA, ON_TOUCH_RUNNABLE,
ON_PRIMARY_ACTION, ON_SECONDARY_ACTION};
} }
...@@ -18,6 +18,13 @@ import androidx.annotation.Nullable; ...@@ -18,6 +18,13 @@ import androidx.annotation.Nullable;
import org.chromium.components.browser_ui.widget.BoundedLinearLayout; import org.chromium.components.browser_ui.widget.BoundedLinearLayout;
import org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener; import org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener;
import org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener.SwipeHandler; import org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener.SwipeHandler;
import org.chromium.components.browser_ui.widget.listmenu.BasicListMenu;
import org.chromium.components.browser_ui.widget.listmenu.ListMenu;
import org.chromium.components.browser_ui.widget.listmenu.ListMenuButton;
import org.chromium.components.browser_ui.widget.listmenu.ListMenuButtonDelegate;
import org.chromium.components.browser_ui.widget.listmenu.ListMenuItemProperties;
import org.chromium.ui.modelutil.MVCListAdapter;
import org.chromium.ui.modelutil.PropertyModel;
/** /**
* View representing the message banner. * View representing the message banner.
...@@ -27,8 +34,10 @@ public class MessageBannerView extends BoundedLinearLayout { ...@@ -27,8 +34,10 @@ public class MessageBannerView extends BoundedLinearLayout {
private TextView mTitle; private TextView mTitle;
private TextView mDescription; private TextView mDescription;
private TextView mPrimaryButton; private TextView mPrimaryButton;
private ImageView mSecondaryButton; private ListMenuButton mSecondaryButton;
private View mDivider; private View mDivider;
private String mSecondaryActionText;
private Runnable mSecondaryActionCallback;
private SwipeGestureListener mSwipeGestureDetector; private SwipeGestureListener mSwipeGestureDetector;
public MessageBannerView(Context context, @Nullable AttributeSet attrs) { public MessageBannerView(Context context, @Nullable AttributeSet attrs) {
...@@ -44,6 +53,7 @@ public class MessageBannerView extends BoundedLinearLayout { ...@@ -44,6 +53,7 @@ public class MessageBannerView extends BoundedLinearLayout {
mIconView = findViewById(R.id.message_icon); mIconView = findViewById(R.id.message_icon);
mSecondaryButton = findViewById(R.id.message_secondary_button); mSecondaryButton = findViewById(R.id.message_secondary_button);
mDivider = findViewById(R.id.message_divider); mDivider = findViewById(R.id.message_divider);
mSecondaryButton.setOnClickListener((View v) -> { displayMenu(); });
} }
void setTitle(String title) { void setTitle(String title) {
...@@ -74,6 +84,14 @@ public class MessageBannerView extends BoundedLinearLayout { ...@@ -74,6 +84,14 @@ public class MessageBannerView extends BoundedLinearLayout {
mDivider.setVisibility(VISIBLE); mDivider.setVisibility(VISIBLE);
} }
void setSecondaryActionCallback(Runnable callback) {
mSecondaryActionCallback = callback;
}
void setSecondaryActionText(String text) {
mSecondaryActionText = text;
}
void setSecondaryIconContentDescription(String description) { void setSecondaryIconContentDescription(String description) {
mSecondaryButton.setContentDescription(description); mSecondaryButton.setContentDescription(description);
} }
...@@ -82,6 +100,39 @@ public class MessageBannerView extends BoundedLinearLayout { ...@@ -82,6 +100,39 @@ public class MessageBannerView extends BoundedLinearLayout {
mSwipeGestureDetector = new MessageSwipeGestureListener(getContext(), handler); mSwipeGestureDetector = new MessageSwipeGestureListener(getContext(), handler);
} }
// TODO(pavely): For the M88 experiment we decided to display single item menu in response to
// the tap on secondary button. The code below implements this logic. Past M88 it will be
// replaced with modal dialog driven from the feature code.
void displayMenu() {
final PropertyModel menuItemPropertyModel =
new PropertyModel.Builder(ListMenuItemProperties.ALL_KEYS)
.with(ListMenuItemProperties.TITLE, mSecondaryActionText)
.with(ListMenuItemProperties.ENABLED, true)
.build();
MVCListAdapter.ModelList menuItems = new MVCListAdapter.ModelList();
menuItems.add(new MVCListAdapter.ListItem(
BasicListMenu.ListMenuItemType.MENU_ITEM, menuItemPropertyModel));
ListMenu.Delegate listMenuDelegate = (PropertyModel menuItem) -> {
assert menuItem == menuItemPropertyModel;
// There is only one menu item in the menu.
if (mSecondaryActionCallback != null) {
mSecondaryActionCallback.run();
}
};
BasicListMenu listMenu = new BasicListMenu(getContext(), menuItems, listMenuDelegate);
ListMenuButtonDelegate delegate = new ListMenuButtonDelegate() {
@Override
public ListMenu getListMenu() {
return listMenu;
}
};
mSecondaryButton.setDelegate(delegate);
mSecondaryButton.showMenu();
}
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
@Override @Override
public boolean onTouchEvent(MotionEvent event) { public boolean onTouchEvent(MotionEvent event) {
......
...@@ -8,11 +8,14 @@ import static org.chromium.components.messages.MessageBannerProperties.ALPHA; ...@@ -8,11 +8,14 @@ import static org.chromium.components.messages.MessageBannerProperties.ALPHA;
import static org.chromium.components.messages.MessageBannerProperties.DESCRIPTION; import static org.chromium.components.messages.MessageBannerProperties.DESCRIPTION;
import static org.chromium.components.messages.MessageBannerProperties.ICON; import static org.chromium.components.messages.MessageBannerProperties.ICON;
import static org.chromium.components.messages.MessageBannerProperties.ICON_RESOURCE_ID; import static org.chromium.components.messages.MessageBannerProperties.ICON_RESOURCE_ID;
import static org.chromium.components.messages.MessageBannerProperties.ON_SECONDARY_ACTION;
import static org.chromium.components.messages.MessageBannerProperties.ON_TOUCH_RUNNABLE; import static org.chromium.components.messages.MessageBannerProperties.ON_TOUCH_RUNNABLE;
import static org.chromium.components.messages.MessageBannerProperties.PRIMARY_BUTTON_CLICK_LISTENER; import static org.chromium.components.messages.MessageBannerProperties.PRIMARY_BUTTON_CLICK_LISTENER;
import static org.chromium.components.messages.MessageBannerProperties.PRIMARY_BUTTON_TEXT; import static org.chromium.components.messages.MessageBannerProperties.PRIMARY_BUTTON_TEXT;
import static org.chromium.components.messages.MessageBannerProperties.SECONDARY_ACTION_TEXT;
import static org.chromium.components.messages.MessageBannerProperties.SECONDARY_ICON; import static org.chromium.components.messages.MessageBannerProperties.SECONDARY_ICON;
import static org.chromium.components.messages.MessageBannerProperties.SECONDARY_ICON_CONTENT_DESCRIPTION; import static org.chromium.components.messages.MessageBannerProperties.SECONDARY_ICON_CONTENT_DESCRIPTION;
import static org.chromium.components.messages.MessageBannerProperties.SECONDARY_ICON_RESOURCE_ID;
import static org.chromium.components.messages.MessageBannerProperties.TITLE; import static org.chromium.components.messages.MessageBannerProperties.TITLE;
import static org.chromium.components.messages.MessageBannerProperties.TRANSLATION_Y; import static org.chromium.components.messages.MessageBannerProperties.TRANSLATION_Y;
...@@ -44,8 +47,15 @@ public class MessageBannerViewBinder { ...@@ -44,8 +47,15 @@ public class MessageBannerViewBinder {
AppCompatResources.getDrawable(view.getContext(), model.get(ICON_RESOURCE_ID))); AppCompatResources.getDrawable(view.getContext(), model.get(ICON_RESOURCE_ID)));
} else if (propertyKey == SECONDARY_ICON) { } else if (propertyKey == SECONDARY_ICON) {
view.setSecondaryIcon(model.get(SECONDARY_ICON)); view.setSecondaryIcon(model.get(SECONDARY_ICON));
} else if (propertyKey == SECONDARY_ICON_RESOURCE_ID) {
view.setSecondaryIcon(AppCompatResources.getDrawable(
view.getContext(), model.get(SECONDARY_ICON_RESOURCE_ID)));
} else if (propertyKey == SECONDARY_ACTION_TEXT) {
view.setSecondaryActionText(model.get(SECONDARY_ACTION_TEXT));
} else if (propertyKey == SECONDARY_ICON_CONTENT_DESCRIPTION) { } else if (propertyKey == SECONDARY_ICON_CONTENT_DESCRIPTION) {
view.setSecondaryIconContentDescription(model.get(SECONDARY_ICON_CONTENT_DESCRIPTION)); view.setSecondaryIconContentDescription(model.get(SECONDARY_ICON_CONTENT_DESCRIPTION));
} else if (propertyKey == ON_SECONDARY_ACTION) {
view.setSecondaryActionCallback(model.get(ON_SECONDARY_ACTION));
} else if (propertyKey == ON_TOUCH_RUNNABLE) { } else if (propertyKey == ON_TOUCH_RUNNABLE) {
Runnable runnable = model.get(ON_TOUCH_RUNNABLE); Runnable runnable = model.get(ON_TOUCH_RUNNABLE);
if (runnable == null) { if (runnable == null) {
......
// 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.components.messages;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import android.app.Activity;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.test.filters.MediumTest;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.chromium.base.test.BaseActivityTestRule;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import org.chromium.ui.test.util.DisableAnimationsTestRule;
import org.chromium.ui.test.util.DummyUiActivity;
/**
* Instrumentation tests for MessageBannerView.
*/
@RunWith(BaseJUnit4ClassRunner.class)
@Batch(Batch.UNIT_TESTS)
public class MessageBannerViewTest {
private static final String SECONDARY_ACTION_TEXT = "SecondaryActionText";
@ClassRule
public static DisableAnimationsTestRule sDisableAnimationsRule =
new DisableAnimationsTestRule();
@ClassRule
public static BaseActivityTestRule<DummyUiActivity> sActivityTestRule =
new BaseActivityTestRule<>(DummyUiActivity.class);
private static Activity sActivity;
private static ViewGroup sContentView;
@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock
Runnable mSecondaryActionCallback;
MessageBannerView mMessageBannerView;
@BeforeClass
public static void setupSuite() {
sActivityTestRule.launchActivity(null);
TestThreadUtils.runOnUiThreadBlocking(() -> {
sActivity = sActivityTestRule.getActivity();
sContentView = new FrameLayout(sActivity);
sActivity.setContentView(sContentView);
});
}
@Before
public void setupTest() throws Exception {
TestThreadUtils.runOnUiThreadBlocking(() -> {
sContentView.removeAllViews();
mMessageBannerView = (MessageBannerView) LayoutInflater.from(sActivity).inflate(
R.layout.message_banner_view, sContentView, false);
sContentView.addView(mMessageBannerView);
});
}
/**
* Tests that clicking on secondary button opens a menu with an item with SECONDARY_ACTION_TEXT.
* Clicking on this item triggers ON_SECONDARY_ACTION callback invocation.
*/
@Test
@MediumTest
public void testSecondaryActionMenu() {
PropertyModel propertyModel =
new PropertyModel.Builder(MessageBannerProperties.SINGLE_ACTION_MESSAGE_KEYS)
.with(MessageBannerProperties.SECONDARY_ICON_RESOURCE_ID,
android.R.drawable.ic_menu_add)
.with(MessageBannerProperties.SECONDARY_ACTION_TEXT, SECONDARY_ACTION_TEXT)
.with(MessageBannerProperties.ON_SECONDARY_ACTION, mSecondaryActionCallback)
.build();
TestThreadUtils.runOnUiThreadBlocking(() -> {
PropertyModelChangeProcessor.create(
propertyModel, mMessageBannerView, MessageBannerViewBinder::bind);
});
onView(withId(R.id.message_secondary_button)).perform(click());
onView(withText(SECONDARY_ACTION_TEXT)).perform(click());
Mockito.verify(mSecondaryActionCallback).run();
}
}
...@@ -34,6 +34,8 @@ public final class MessageWrapper { ...@@ -34,6 +34,8 @@ public final class MessageWrapper {
mMessageProperties = mMessageProperties =
new PropertyModel.Builder(MessageBannerProperties.SINGLE_ACTION_MESSAGE_KEYS) new PropertyModel.Builder(MessageBannerProperties.SINGLE_ACTION_MESSAGE_KEYS)
.with(MessageBannerProperties.ON_PRIMARY_ACTION, this::handleActionClick) .with(MessageBannerProperties.ON_PRIMARY_ACTION, this::handleActionClick)
.with(MessageBannerProperties.ON_SECONDARY_ACTION,
this::handleSecondaryActionClick)
.with(MessageBannerProperties.ON_DISMISSED, this::handleMessageDismissed) .with(MessageBannerProperties.ON_DISMISSED, this::handleMessageDismissed)
.build(); .build();
} }
...@@ -72,6 +74,16 @@ public final class MessageWrapper { ...@@ -72,6 +74,16 @@ public final class MessageWrapper {
mMessageProperties.set(MessageBannerProperties.PRIMARY_BUTTON_TEXT, primaryButtonText); mMessageProperties.set(MessageBannerProperties.PRIMARY_BUTTON_TEXT, primaryButtonText);
} }
@CalledByNative
String getSecondaryActionText() {
return mMessageProperties.get(MessageBannerProperties.SECONDARY_ACTION_TEXT);
}
@CalledByNative
void setSecondaryActionText(String secondaryActionText) {
mMessageProperties.set(MessageBannerProperties.SECONDARY_ACTION_TEXT, secondaryActionText);
}
@CalledByNative @CalledByNative
@DrawableRes @DrawableRes
int getIconResourceId() { int getIconResourceId() {
...@@ -83,6 +95,17 @@ public final class MessageWrapper { ...@@ -83,6 +95,17 @@ public final class MessageWrapper {
mMessageProperties.set(MessageBannerProperties.ICON_RESOURCE_ID, resourceId); mMessageProperties.set(MessageBannerProperties.ICON_RESOURCE_ID, resourceId);
} }
@CalledByNative
@DrawableRes
int getSecondaryIconResourceId() {
return mMessageProperties.get(MessageBannerProperties.SECONDARY_ICON_RESOURCE_ID);
}
@CalledByNative
void setSecondaryIconResourceId(@DrawableRes int resourceId) {
mMessageProperties.set(MessageBannerProperties.SECONDARY_ICON_RESOURCE_ID, resourceId);
}
@CalledByNative @CalledByNative
void clearNativePtr() { void clearNativePtr() {
mNativeMessageWrapper = 0; mNativeMessageWrapper = 0;
...@@ -93,6 +116,11 @@ public final class MessageWrapper { ...@@ -93,6 +116,11 @@ public final class MessageWrapper {
MessageWrapperJni.get().handleActionClick(mNativeMessageWrapper); MessageWrapperJni.get().handleActionClick(mNativeMessageWrapper);
} }
private void handleSecondaryActionClick() {
if (mNativeMessageWrapper == 0) return;
MessageWrapperJni.get().handleSecondaryActionClick(mNativeMessageWrapper);
}
private void handleMessageDismissed() { private void handleMessageDismissed() {
// mNativeMessageWrapper can be null if the message was dismissed from native API. // mNativeMessageWrapper can be null if the message was dismissed from native API.
// In this case dismiss callback should have already been called. // In this case dismiss callback should have already been called.
...@@ -103,6 +131,7 @@ public final class MessageWrapper { ...@@ -103,6 +131,7 @@ public final class MessageWrapper {
@NativeMethods @NativeMethods
interface Natives { interface Natives {
void handleActionClick(long nativeMessageWrapper); void handleActionClick(long nativeMessageWrapper);
void handleSecondaryActionClick(long nativeMessageWrapper);
void handleDismissCallback(long nativeMessageWrapper); void handleDismissCallback(long nativeMessageWrapper);
} }
} }
\ No newline at end of file
...@@ -62,9 +62,17 @@ public class MessageWrapperTest { ...@@ -62,9 +62,17 @@ public class MessageWrapperTest {
Assert.assertEquals("Button text doesn't match provided value", "Primary button", Assert.assertEquals("Button text doesn't match provided value", "Primary button",
messageProperties.get(MessageBannerProperties.PRIMARY_BUTTON_TEXT)); messageProperties.get(MessageBannerProperties.PRIMARY_BUTTON_TEXT));
message.setSecondaryActionText("Primary button");
Assert.assertEquals("Button text doesn't match provided value", "Primary button",
messageProperties.get(MessageBannerProperties.SECONDARY_ACTION_TEXT));
message.setIconResourceId(1); message.setIconResourceId(1);
Assert.assertEquals("Icon resource id doesn't match provided value", 1, Assert.assertEquals("Icon resource id doesn't match provided value", 1,
messageProperties.get(MessageBannerProperties.ICON_RESOURCE_ID)); messageProperties.get(MessageBannerProperties.ICON_RESOURCE_ID));
message.setSecondaryIconResourceId(2);
Assert.assertEquals("Icon resource id doesn't match provided value", 2,
messageProperties.get(MessageBannerProperties.SECONDARY_ICON_RESOURCE_ID));
} }
/** /**
...@@ -78,6 +86,8 @@ public class MessageWrapperTest { ...@@ -78,6 +86,8 @@ public class MessageWrapperTest {
PropertyModel messageProperties = message.getMessageProperties(); PropertyModel messageProperties = message.getMessageProperties();
messageProperties.get(MessageBannerProperties.ON_PRIMARY_ACTION).run(); messageProperties.get(MessageBannerProperties.ON_PRIMARY_ACTION).run();
Mockito.verify(mNativeMock).handleActionClick(nativePtr); Mockito.verify(mNativeMock).handleActionClick(nativePtr);
messageProperties.get(MessageBannerProperties.ON_SECONDARY_ACTION).run();
Mockito.verify(mNativeMock).handleSecondaryActionClick(nativePtr);
messageProperties.get(MessageBannerProperties.ON_DISMISSED).run(); messageProperties.get(MessageBannerProperties.ON_DISMISSED).run();
Mockito.verify(mNativeMock).handleDismissCallback(nativePtr); Mockito.verify(mNativeMock).handleDismissCallback(nativePtr);
} }
...@@ -95,6 +105,8 @@ public class MessageWrapperTest { ...@@ -95,6 +105,8 @@ public class MessageWrapperTest {
message.clearNativePtr(); message.clearNativePtr();
messageProperties.get(MessageBannerProperties.ON_PRIMARY_ACTION).run(); messageProperties.get(MessageBannerProperties.ON_PRIMARY_ACTION).run();
Mockito.verify(mNativeMock, never()).handleActionClick(nativePtr); Mockito.verify(mNativeMock, never()).handleActionClick(nativePtr);
messageProperties.get(MessageBannerProperties.ON_SECONDARY_ACTION).run();
Mockito.verify(mNativeMock, never()).handleSecondaryActionClick(nativePtr);
messageProperties.get(MessageBannerProperties.ON_DISMISSED).run(); messageProperties.get(MessageBannerProperties.ON_DISMISSED).run();
Mockito.verify(mNativeMock, never()).handleDismissCallback(nativePtr); Mockito.verify(mNativeMock, never()).handleDismissCallback(nativePtr);
} }
......
...@@ -74,6 +74,24 @@ void MessageWrapper::SetPrimaryButtonText( ...@@ -74,6 +74,24 @@ void MessageWrapper::SetPrimaryButtonText(
jprimary_button_text); jprimary_button_text);
} }
base::string16 MessageWrapper::GetSecondaryActionText() {
JNIEnv* env = base::android::AttachCurrentThread();
base::android::ScopedJavaLocalRef<jstring> jsecondary_action_text =
Java_MessageWrapper_getSecondaryActionText(env, java_message_wrapper_);
return jsecondary_action_text.is_null()
? base::string16()
: base::android::ConvertJavaStringToUTF16(jsecondary_action_text);
}
void MessageWrapper::SetSecondaryActionText(
const base::string16& secondary_action_text) {
JNIEnv* env = base::android::AttachCurrentThread();
base::android::ScopedJavaLocalRef<jstring> jsecondary_action_text =
base::android::ConvertUTF16ToJavaString(env, secondary_action_text);
Java_MessageWrapper_setSecondaryActionText(env, java_message_wrapper_,
jsecondary_action_text);
}
int MessageWrapper::GetIconResourceId() { int MessageWrapper::GetIconResourceId() {
JNIEnv* env = base::android::AttachCurrentThread(); JNIEnv* env = base::android::AttachCurrentThread();
return Java_MessageWrapper_getIconResourceId(env, java_message_wrapper_); return Java_MessageWrapper_getIconResourceId(env, java_message_wrapper_);
...@@ -85,11 +103,32 @@ void MessageWrapper::SetIconResourceId(int resource_id) { ...@@ -85,11 +103,32 @@ void MessageWrapper::SetIconResourceId(int resource_id) {
resource_id); resource_id);
} }
int MessageWrapper::GetSecondaryIconResourceId() {
JNIEnv* env = base::android::AttachCurrentThread();
return Java_MessageWrapper_getSecondaryIconResourceId(env,
java_message_wrapper_);
}
void MessageWrapper::SetSecondaryIconResourceId(int resource_id) {
JNIEnv* env = base::android::AttachCurrentThread();
Java_MessageWrapper_setSecondaryIconResourceId(env, java_message_wrapper_,
resource_id);
}
void MessageWrapper::SetSecondaryActionCallback(base::OnceClosure callback) {
secondary_action_callback_ = std::move(callback);
}
void MessageWrapper::HandleActionClick(JNIEnv* env) { void MessageWrapper::HandleActionClick(JNIEnv* env) {
if (!action_callback_.is_null()) if (!action_callback_.is_null())
std::move(action_callback_).Run(); std::move(action_callback_).Run();
} }
void MessageWrapper::HandleSecondaryActionClick(JNIEnv* env) {
if (!secondary_action_callback_.is_null())
std::move(secondary_action_callback_).Run();
}
void MessageWrapper::HandleDismissCallback(JNIEnv* env) { void MessageWrapper::HandleDismissCallback(JNIEnv* env) {
// Make sure message dismissed callback is called exactly once. // Make sure message dismissed callback is called exactly once.
CHECK(!message_dismissed_); CHECK(!message_dismissed_);
......
...@@ -39,14 +39,21 @@ class MessageWrapper { ...@@ -39,14 +39,21 @@ class MessageWrapper {
void SetDescription(const base::string16& description); void SetDescription(const base::string16& description);
base::string16 GetPrimaryButtonText(); base::string16 GetPrimaryButtonText();
void SetPrimaryButtonText(const base::string16& primary_button_text); void SetPrimaryButtonText(const base::string16& primary_button_text);
base::string16 GetSecondaryActionText();
void SetSecondaryActionText(const base::string16& secondary_action_text);
int GetIconResourceId();
// When setting a message icon use ResourceMapper::MapToJavaDrawableId to // When setting a message icon use ResourceMapper::MapToJavaDrawableId to
// translate from chromium resource_id to Android drawable resource_id. // translate from chromium resource_id to Android drawable resource_id.
int GetIconResourceId();
void SetIconResourceId(int resource_id); void SetIconResourceId(int resource_id);
int GetSecondaryIconResourceId();
void SetSecondaryIconResourceId(int resource_id);
void SetSecondaryActionCallback(base::OnceClosure callback);
// Following methods forward calls from java to provided callbacks. // Following methods forward calls from java to provided callbacks.
void HandleActionClick(JNIEnv* env); void HandleActionClick(JNIEnv* env);
void HandleSecondaryActionClick(JNIEnv* env);
void HandleDismissCallback(JNIEnv* env); void HandleDismissCallback(JNIEnv* env);
const base::android::JavaRef<jobject>& GetJavaMessageWrapper() const; const base::android::JavaRef<jobject>& GetJavaMessageWrapper() const;
...@@ -54,6 +61,7 @@ class MessageWrapper { ...@@ -54,6 +61,7 @@ class MessageWrapper {
private: private:
base::android::ScopedJavaGlobalRef<jobject> java_message_wrapper_; base::android::ScopedJavaGlobalRef<jobject> java_message_wrapper_;
base::OnceClosure action_callback_; base::OnceClosure action_callback_;
base::OnceClosure secondary_action_callback_;
base::OnceClosure dismiss_callback_; base::OnceClosure dismiss_callback_;
bool message_dismissed_; bool message_dismissed_;
}; };
......
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