Commit f3c5839b authored by David Maunder's avatar David Maunder Committed by Commit Bot

Implement UI for price drops

Show in tab grid card the old and new price for a shopping offer when
there is a reduction in the price.

Bug: 1145766
Change-Id: I20d42f2731ff7759f452d2cd2d3cb5331bfc1d5f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2520167
Commit-Queue: David Maunder <davidjm@chromium.org>
Reviewed-by: default avatarYusuf Ozuysal <yusufo@chromium.org>
Reviewed-by: default avatarLijin Shen <lazzzis@google.com>
Reviewed-by: default avatarMei Liang <meiliang@chromium.org>
Cr-Commit-Position: refs/heads/master@{#825202}
parent 9b7c3260
...@@ -41,6 +41,8 @@ android_resources("java_resources") { ...@@ -41,6 +41,8 @@ android_resources("java_resources") {
"java/res/drawable/message_card_background_with_inset.xml", "java/res/drawable/message_card_background_with_inset.xml",
"java/res/drawable/message_card_background_with_inset_incognito.xml", "java/res/drawable/message_card_background_with_inset_incognito.xml",
"java/res/drawable/popup_bg_dark.xml", "java/res/drawable/popup_bg_dark.xml",
"java/res/drawable/price_card_background.xml",
"java/res/drawable/price_card_scrim.xml",
"java/res/drawable/selected_tab_background.xml", "java/res/drawable/selected_tab_background.xml",
"java/res/drawable/selected_tab_background_incognito.xml", "java/res/drawable/selected_tab_background_incognito.xml",
"java/res/drawable/single_tab_background.xml", "java/res/drawable/single_tab_background.xml",
...@@ -61,6 +63,7 @@ android_resources("java_resources") { ...@@ -61,6 +63,7 @@ android_resources("java_resources") {
"java/res/layout/incognito_description_container_layout.xml", "java/res/layout/incognito_description_container_layout.xml",
"java/res/layout/iph_drag_and_drop_dialog_layout.xml", "java/res/layout/iph_drag_and_drop_dialog_layout.xml",
"java/res/layout/new_tab_tile_card_item.xml", "java/res/layout/new_tab_tile_card_item.xml",
"java/res/layout/price_card.xml",
"java/res/layout/selectable_tab_grid_card_item.xml", "java/res/layout/selectable_tab_grid_card_item.xml",
"java/res/layout/selectable_tab_list_card_item.xml", "java/res/layout/selectable_tab_list_card_item.xml",
"java/res/layout/single_tab_view_layout.xml", "java/res/layout/single_tab_view_layout.xml",
...@@ -119,6 +122,7 @@ android_library("java") { ...@@ -119,6 +122,7 @@ android_library("java") {
"java/src/org/chromium/chrome/browser/tasks/tab_management/NewTabTileView.java", "java/src/org/chromium/chrome/browser/tasks/tab_management/NewTabTileView.java",
"java/src/org/chromium/chrome/browser/tasks/tab_management/NewTabTileViewBinder.java", "java/src/org/chromium/chrome/browser/tasks/tab_management/NewTabTileViewBinder.java",
"java/src/org/chromium/chrome/browser/tasks/tab_management/NewTabTileViewProperties.java", "java/src/org/chromium/chrome/browser/tasks/tab_management/NewTabTileViewProperties.java",
"java/src/org/chromium/chrome/browser/tasks/tab_management/PriceCardView.java",
"java/src/org/chromium/chrome/browser/tasks/tab_management/SelectableTabGridView.java", "java/src/org/chromium/chrome/browser/tasks/tab_management/SelectableTabGridView.java",
"java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogCoordinator.java", "java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogCoordinator.java",
"java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMediator.java", "java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMediator.java",
......
<?xml version="1.0" encoding="utf-8"?>
<!-- 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. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/green_50"/>
<corners android:radius="@dimen/tab_grid_price_card_radius"/>
</shape>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:startColor="@color/black_alpha_38"
android:endColor="@android:color/transparent"
android:angle="270"
android:type="linear"/>
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!-- 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. -->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:id="@+id/price_info_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginStart="4dp"
android:orientation="horizontal"
android:background="@drawable/price_card_background">
<TextView
android:id="@+id/current_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.TextMediumThick.Green.Dark"
android:textAlignment="viewStart" />
<TextView
android:id="@+id/previous_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="4dp"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.TextMedium.Secondary.Dark"
android:textAlignment="viewStart" />
</LinearLayout>
</merge>
\ No newline at end of file
...@@ -63,6 +63,13 @@ ...@@ -63,6 +63,13 @@
android:src="@color/thumbnail_placeholder_on_primary_bg" android:src="@color/thumbnail_placeholder_on_primary_bg"
app:cornerRadiusBottomStart="@dimen/tab_list_card_radius" app:cornerRadiusBottomStart="@dimen/tab_list_card_radius"
app:cornerRadiusBottomEnd="@dimen/tab_list_card_radius"/> app:cornerRadiusBottomEnd="@dimen/tab_list_card_radius"/>
<org.chromium.chrome.browser.tasks.tab_management.PriceCardView
android:id="@+id/price_info_box_outer"
android:layout_below="@id/tab_title"
android:background="@drawable/price_card_scrim"
android:layout_width="match_parent"
android:layout_height="56dp"
android:visibility="gone"/>
<View <View
android:id="@+id/divider_view" android:id="@+id/divider_view"
style="@style/HorizontalDivider" style="@style/HorizontalDivider"
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
<dimen name="tab_list_selected_inset_low_end">6dp</dimen> <dimen name="tab_list_selected_inset_low_end">6dp</dimen>
<dimen name="tab_list_card_padding">8dp</dimen> <dimen name="tab_list_card_padding">8dp</dimen>
<dimen name="tab_list_card_radius">4dp</dimen> <dimen name="tab_list_card_radius">4dp</dimen>
<dimen name="tab_grid_price_card_radius">4dp</dimen>
<dimen name="tab_list_mini_card_radius">4dp</dimen> <dimen name="tab_list_mini_card_radius">4dp</dimen>
<dimen name="tab_list_mini_card_frame_size">1dp</dimen> <dimen name="tab_list_mini_card_frame_size">1dp</dimen>
<dimen name="tab_grid_bottom_padding">16dp</dimen> <dimen name="tab_grid_bottom_padding">16dp</dimen>
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.tasks.tab_management;
import android.content.Context;
import android.graphics.Paint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.chromium.chrome.tab_ui.R;
/**
* Contains pricing information relating to a shopping offer website. Currently only
* supports displaying the old and new price of the offer when a price drop is detected.
*/
public class PriceCardView extends FrameLayout {
private TextView mPriceInfoBox;
private TextView mPreviousPriceInfoBox;
public PriceCardView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Sets the current price string and previous price string when a price drop is detected
*/
public void setPriceStrings(String priceString, String previousPriceString) {
assert !TextUtils.isEmpty(priceString) && !TextUtils.isEmpty(previousPriceString);
mPriceInfoBox.setText(priceString);
mPreviousPriceInfoBox.setText(previousPriceString);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
LayoutInflater.from(getContext()).inflate(R.layout.price_card, this);
mPriceInfoBox = (TextView) findViewById(R.id.current_price);
mPreviousPriceInfoBox = (TextView) findViewById(R.id.previous_price);
mPreviousPriceInfoBox.setPaintFlags(
mPreviousPriceInfoBox.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
}
}
\ No newline at end of file
...@@ -220,16 +220,15 @@ class TabGridViewBinder { ...@@ -220,16 +220,15 @@ class TabGridViewBinder {
} else if (TabProperties.SHOPPING_PERSISTED_TAB_DATA_FETCHER == propertyKey) { } else if (TabProperties.SHOPPING_PERSISTED_TAB_DATA_FETCHER == propertyKey) {
model.get(TabProperties.SHOPPING_PERSISTED_TAB_DATA_FETCHER) model.get(TabProperties.SHOPPING_PERSISTED_TAB_DATA_FETCHER)
.fetch((shoppingPersistedTabData) -> { .fetch((shoppingPersistedTabData) -> {
ChipView pageInfoButton = PriceCardView priceCardView =
(ChipView) view.fastFindViewById(R.id.page_info_button); (PriceCardView) view.fastFindViewById(R.id.price_info_box_outer);
if (TextUtils.isEmpty(shoppingPersistedTabData.getPriceString())) { if (shoppingPersistedTabData.getPriceDrop() == null) {
pageInfoButton.setVisibility(View.GONE); priceCardView.setVisibility(View.GONE);
} else { } else {
// Price string and search query are mutually exclusive priceCardView.setPriceStrings(
assert TextUtils.isEmpty(model.get(TabProperties.SEARCH_QUERY)); shoppingPersistedTabData.getPriceDrop().integerPrice,
pageInfoButton.setVisibility(View.VISIBLE); shoppingPersistedTabData.getPriceDrop().previousIntegerPrice);
pageInfoButton.getPrimaryTextView().setText( priceCardView.setVisibility(View.VISIBLE);
shoppingPersistedTabData.getPriceString());
} }
}); });
} else if (TabProperties.PAGE_INFO_LISTENER == propertyKey) { } else if (TabProperties.PAGE_INFO_LISTENER == propertyKey) {
......
...@@ -56,7 +56,8 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -56,7 +56,8 @@ import java.util.concurrent.atomic.AtomicInteger;
public class TabListViewHolderTest extends DummyUiActivityTestCase { public class TabListViewHolderTest extends DummyUiActivityTestCase {
private static final int TAB1_ID = 456; private static final int TAB1_ID = 456;
private static final int TAB2_ID = 789; private static final int TAB2_ID = 789;
private static final String EXPECTED_PRICE_STRING = "$2.87"; private static final String EXPECTED_PRICE_STRING = "$287";
private static final String EXPECTED_PREVIOUS_PRICE_STRING = "$314";
private ViewGroup mTabGridView; private ViewGroup mTabGridView;
private PropertyModel mGridModel; private PropertyModel mGridModel;
...@@ -558,21 +559,70 @@ public class TabListViewHolderTest extends DummyUiActivityTestCase { ...@@ -558,21 +559,70 @@ public class TabListViewHolderTest extends DummyUiActivityTestCase {
@Test @Test
@MediumTest @MediumTest
@UiThreadTest @UiThreadTest
public void testPriceString() { public void testPriceStringPriceDrop() {
TabUiFeatureUtilities.ENABLE_PRICE_TRACKING.setForTesting(true); Tab tab = MockTab.createAndInitialize(1, false);
testGridSelected(mTabGridView, mGridModel); MockShoppingPersistedTabDataFetcher fetcher = new MockShoppingPersistedTabDataFetcher(tab);
ChipView pageInfoButton = mTabGridView.findViewById(R.id.page_info_button); fetcher.setPriceStrings(EXPECTED_PRICE_STRING, EXPECTED_PREVIOUS_PRICE_STRING);
testPriceString(
tab, fetcher, View.VISIBLE, EXPECTED_PRICE_STRING, EXPECTED_PREVIOUS_PRICE_STRING);
}
@Test
@MediumTest
@UiThreadTest
public void testPriceStringNullPriceDrop() {
Tab tab = MockTab.createAndInitialize(1, false); Tab tab = MockTab.createAndInitialize(1, false);
MockShoppingPersistedTabDataFetcher fetcher = new MockShoppingPersistedTabDataFetcher(tab); MockShoppingPersistedTabDataFetcher fetcher = new MockShoppingPersistedTabDataFetcher(tab);
fetcher.setPriceString(EXPECTED_PRICE_STRING); fetcher.setNullPriceDrop();
mGridModel.set(TabProperties.SHOPPING_PERSISTED_TAB_DATA_FETCHER, fetcher); testPriceString(
Assert.assertEquals(View.VISIBLE, pageInfoButton.getVisibility()); tab, fetcher, View.GONE, EXPECTED_PRICE_STRING, EXPECTED_PREVIOUS_PRICE_STRING);
Assert.assertEquals(EXPECTED_PRICE_STRING, pageInfoButton.getPrimaryTextView().getText()); }
@Test
@MediumTest
@UiThreadTest
public void testPriceStringPriceDropThenNull() {
Tab tab = MockTab.createAndInitialize(1, false);
MockShoppingPersistedTabDataFetcher fetcher = new MockShoppingPersistedTabDataFetcher(tab);
fetcher.setPriceStrings(EXPECTED_PRICE_STRING, EXPECTED_PREVIOUS_PRICE_STRING);
testPriceString(
tab, fetcher, View.VISIBLE, EXPECTED_PRICE_STRING, EXPECTED_PREVIOUS_PRICE_STRING);
fetcher.setNullPriceDrop();
testPriceString(
tab, fetcher, View.GONE, EXPECTED_PRICE_STRING, EXPECTED_PREVIOUS_PRICE_STRING);
}
private void testPriceString(Tab tab, MockShoppingPersistedTabDataFetcher fetcher,
int expectedVisibility, String expectedCurrentPrice, String expectedPreviousPrice) {
TabUiFeatureUtilities.ENABLE_PRICE_TRACKING.setForTesting(true);
testGridSelected(mTabGridView, mGridModel);
PriceCardView priceCardView = mTabGridView.findViewById(R.id.price_info_box_outer);
TextView currentPrice = mTabGridView.findViewById(R.id.current_price);
TextView previousPrice = mTabGridView.findViewById(R.id.previous_price);
fetcher.setPriceString("");
mGridModel.set(TabProperties.SHOPPING_PERSISTED_TAB_DATA_FETCHER, fetcher); mGridModel.set(TabProperties.SHOPPING_PERSISTED_TAB_DATA_FETCHER, fetcher);
Assert.assertEquals(View.GONE, pageInfoButton.getVisibility()); Assert.assertEquals(expectedVisibility, priceCardView.getVisibility());
if (expectedVisibility == View.VISIBLE) {
Assert.assertEquals(expectedCurrentPrice, currentPrice.getText());
Assert.assertEquals(expectedPreviousPrice, previousPrice.getText());
}
}
static class MockShoppingPersistedTabData extends ShoppingPersistedTabData {
private PriceDrop mPriceDrop;
MockShoppingPersistedTabData(Tab tab) {
super(tab);
}
public void setPriceStrings(String priceString, String previousPriceString) {
mPriceDrop = new PriceDrop(priceString, previousPriceString);
}
@Override
public PriceDrop getPriceDrop() {
return mPriceDrop;
}
} }
/** /**
...@@ -580,19 +630,24 @@ public class TabListViewHolderTest extends DummyUiActivityTestCase { ...@@ -580,19 +630,24 @@ public class TabListViewHolderTest extends DummyUiActivityTestCase {
*/ */
static class MockShoppingPersistedTabDataFetcher static class MockShoppingPersistedTabDataFetcher
extends TabListMediator.ShoppingPersistedTabDataFetcher { extends TabListMediator.ShoppingPersistedTabDataFetcher {
private ShoppingPersistedTabData mShoppingPersistedTabData;
MockShoppingPersistedTabDataFetcher(Tab tab) { MockShoppingPersistedTabDataFetcher(Tab tab) {
super(tab); super(tab);
} }
public void setPriceString(String priceString) {
ShoppingPersistedTabData shoppingPersistedTabData = new ShoppingPersistedTabData(mTab); public void setPriceStrings(String priceString, String previousPriceString) {
shoppingPersistedTabData.setPriceString(priceString, null); mShoppingPersistedTabData = new MockShoppingPersistedTabData(mTab);
mTab.getUserDataHost().setUserData( ((MockShoppingPersistedTabData) mShoppingPersistedTabData)
ShoppingPersistedTabData.class, shoppingPersistedTabData); .setPriceStrings(priceString, previousPriceString);
}
public void setNullPriceDrop() {
mShoppingPersistedTabData = new MockShoppingPersistedTabData(mTab);
} }
@Override @Override
public void fetch(Callback<ShoppingPersistedTabData> callback) { public void fetch(Callback<ShoppingPersistedTabData> callback) {
callback.onResult(mTab.getUserDataHost().getUserData(ShoppingPersistedTabData.class)); callback.onResult(mShoppingPersistedTabData);
} }
} }
......
...@@ -60,6 +60,24 @@ public class ShoppingPersistedTabData extends PersistedTabData { ...@@ -60,6 +60,24 @@ public class ShoppingPersistedTabData extends PersistedTabData {
private String mPreviousPriceString = ""; private String mPreviousPriceString = "";
public long mLastPriceChangeTimeMs = NO_TRANSITIONS_OCCURRED; public long mLastPriceChangeTimeMs = NO_TRANSITIONS_OCCURRED;
/**
* A price drop for the offer {@link ShoppingPersistedTabData}
* refers to
*/
public static class PriceDrop {
public final String integerPrice;
public final String previousIntegerPrice;
/**
* @param integerPrice integer representation of the price
* @param previousIntegerPrice integer representation of the previous price
*/
public PriceDrop(String integerPrice, String previousIntegerPrice) {
this.integerPrice = integerPrice;
this.previousIntegerPrice = previousIntegerPrice;
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
public ShoppingPersistedTabData(Tab tab) { public ShoppingPersistedTabData(Tab tab) {
super(tab, super(tab,
...@@ -169,6 +187,18 @@ public class ShoppingPersistedTabData extends PersistedTabData { ...@@ -169,6 +187,18 @@ public class ShoppingPersistedTabData extends PersistedTabData {
return mPreviousPriceString; return mPreviousPriceString;
} }
/**
* @return {@link PriceDrop} relating to the offer for the {@link ShoppingPersistedTabData}
* TODO(crbug.com/1145770) Implement getPriceDrop to only return a result if there is
* actually a price drop. Ensure priceString and previousPriceString are integers.
* Deprecate getPrice and getPriceString(). Change price and previousPriceString
* representations to be numeric to make drop comparison easier.
*/
public PriceDrop getPriceDrop() {
// TODO(crbug.com/1146609) only show price drops above certain thresholds
return new PriceDrop(mPriceString, mPreviousPriceString);
}
@Override @Override
public byte[] serialize() { public byte[] serialize() {
return ShoppingPersistedTabDataProto.newBuilder() return ShoppingPersistedTabDataProto.newBuilder()
......
...@@ -313,6 +313,13 @@ ...@@ -313,6 +313,13 @@
<item name="android:textColor">@color/blue_when_enabled</item> <item name="android:textColor">@color/blue_when_enabled</item>
</style> </style>
<style name="TextAppearance.TextMediumThick.Green">
<item name="android:textColor">@color/default_green</item>
</style>
<style name="TextAppearance.TextMediumThick.Green.Dark">
<item name="android:textColor">@color/default_green_dark</item>
</style>
<style name="TextAppearance.Button.Text.Blue" parent="TextAppearance.TextAccentMediumThick" <style name="TextAppearance.Button.Text.Blue" parent="TextAppearance.TextAccentMediumThick"
tools:ignore="UnusedResources"> tools:ignore="UnusedResources">
<item name="android:textColor">@color/blue_when_enabled</item> <item name="android:textColor">@color/blue_when_enabled</item>
......
...@@ -21,6 +21,8 @@ ...@@ -21,6 +21,8 @@
<color name="black_alpha_20" tools:ignore="UnusedResources">#33000000</color> <color name="black_alpha_20" tools:ignore="UnusedResources">#33000000</color>
<color name="black_alpha_30" tools:ignore="UnusedResources">#4D000000</color> <color name="black_alpha_30" tools:ignore="UnusedResources">#4D000000</color>
<color name="green_50">#E6F4EA</color>
<color name="white_alpha_65" tools:ignore="UnusedResources">#A6FFFFFF</color> <color name="white_alpha_65" tools:ignore="UnusedResources">#A6FFFFFF</color>
<color name="white_alpha_75" tools:ignore="UnusedResources">#BFFFFFFF</color> <color name="white_alpha_75" tools:ignore="UnusedResources">#BFFFFFFF</color>
......
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