Skip to content

Commit

Permalink
Implement Subscriptions Service Fetcher
Browse files Browse the repository at this point in the history
This CL introduces CommerceSubscriptionsServiceProxy which abstracts the
communication between the client and the subscriptions backend. The CL
also extends BuyableProductPageAnnotation to expose main offer id.

Bug: 1195241
Change-Id: Ia53cf18ce0310bd02d18f4e6b2725732de7866ef
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2801032
Reviewed-by: David Trainor <dtrainor@chromium.org>
Reviewed-by: Yue Zhang <yuezhanggg@chromium.org>
Reviewed-by: David Maunder <davidjm@chromium.org>
Commit-Queue: Ayman Almadhoun <ayman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#869061}
  • Loading branch information
Ayman Almadhoun authored and Chromium LUCI CQ committed Apr 5, 2021
1 parent 4534b37 commit 37c1f35
Show file tree
Hide file tree
Showing 21 changed files with 770 additions and 42 deletions.
Expand Up @@ -16,7 +16,7 @@
import org.chromium.chrome.browser.lens.LensFeature;
import org.chromium.chrome.browser.page_annotations.PageAnnotationsServiceConfig;
import org.chromium.chrome.browser.paint_preview.StartupPaintPreviewHelper;
import org.chromium.chrome.browser.subscriptions.ImplicitPriceDropSubscriptionsManager;
import org.chromium.chrome.browser.subscriptions.CommerceSubscriptionsServiceConfig;
import org.chromium.chrome.browser.tab.state.ShoppingPersistedTabData;
import org.chromium.chrome.browser.tasks.ConditionalTabStripUtils;
import org.chromium.chrome.browser.tasks.ReturnToChromeExperimentsUtil;
Expand Down Expand Up @@ -105,7 +105,6 @@ public void cacheNativeFlags() {
ConditionalTabStripUtils.CONDITIONAL_TAB_STRIP_INFOBAR_LIMIT,
ConditionalTabStripUtils.CONDITIONAL_TAB_STRIP_INFOBAR_PERIOD,
ConditionalTabStripUtils.CONDITIONAL_TAB_STRIP_SESSION_TIME_MS,
ImplicitPriceDropSubscriptionsManager.STALE_TAB_LOWER_BOUND_SECONDS,
LensFeature.DISABLE_LENS_CAMERA_ASSISTED_SEARCH_ON_INCOGNITO,
LensFeature.ENABLE_LENS_CAMERA_ASSISTED_SEARCH_ON_LOW_END_DEVICE,
LensFeature.ENABLE_LENS_CAMERA_ASSISTED_SEARCH_ON_TABLET,
Expand All @@ -131,6 +130,8 @@ public void cacheNativeFlags() {
StartSurfaceConfiguration.TRENDY_FAILURE_MIN_PERIOD_MS,
StartSurfaceConfiguration.TRENDY_SUCCESS_MIN_PERIOD_MS,
StartupPaintPreviewHelper.ACCESSIBILITY_SUPPORT_PARAM,
CommerceSubscriptionsServiceConfig.STALE_TAB_LOWER_BOUND_SECONDS,
CommerceSubscriptionsServiceConfig.SUBSCRIPTIONS_SERVICE_BASE_URL,
TabContentManager.ALLOW_TO_REFETCH_TAB_THUMBNAIL_VARIATION,
TabUiFeatureUtilities.ENABLE_LAUNCH_BUG_FIX,
TabUiFeatureUtilities.ENABLE_LAUNCH_POLISH,
Expand Down
Expand Up @@ -489,4 +489,23 @@ public void testSerializationBug() {
new ShoppingPersistedTabData(tab, serialized, config.getStorage(), config.getId());
Assert.assertEquals(42_000_000L, deserialized.getPriceMicros());
}

@UiThreadTest
@SmallTest
@Test
public void testSerializeWithOfferId() {
Tab tab = new MockTab(ShoppingPersistedTabDataTestUtils.TAB_ID,
ShoppingPersistedTabDataTestUtils.IS_INCOGNITO);
ShoppingPersistedTabData shoppingPersistedTabData = new ShoppingPersistedTabData(tab);
ObservableSupplierImpl<Boolean> supplier = new ObservableSupplierImpl<>();
supplier.set(true);
shoppingPersistedTabData.registerIsTabSaveEnabledSupplier(supplier);
shoppingPersistedTabData.setMainOfferId(ShoppingPersistedTabDataTestUtils.FAKE_OFFER_ID);

byte[] serialized = shoppingPersistedTabData.getSerializeSupplier().get();
ShoppingPersistedTabData deserialized = new ShoppingPersistedTabData(tab);
deserialized.deserialize(serialized);
Assert.assertEquals(
ShoppingPersistedTabDataTestUtils.FAKE_OFFER_ID, deserialized.getMainOfferId());
}
}
Expand Up @@ -67,6 +67,7 @@ public abstract class ShoppingPersistedTabDataTestUtils {
static final String JAPAN_CURRENCY_CODE = "JPY";
static final int TAB_ID = 1;
static final boolean IS_INCOGNITO = false;
static final String FAKE_OFFER_ID = "100";

static ShoppingPersistedTabData createShoppingPersistedTabDataWithDefaults() {
ShoppingPersistedTabData shoppingPersistedTabData =
Expand Down Expand Up @@ -138,15 +139,15 @@ public Void answer(InvocationOnMock invocation) {
switch (expectedResponse) {
case MockPageAnnotationsResponse.BUYABLE_PRODUCT_INITIAL:
add(new BuyableProductPageAnnotation(
PRICE_MICROS, UNITED_STATES_CURRENCY_CODE));
PRICE_MICROS, UNITED_STATES_CURRENCY_CODE, FAKE_OFFER_ID));
break;
case MockPageAnnotationsResponse.BUYABLE_PRODUCT_PRICE_UPDATED:
add(new BuyableProductPageAnnotation(
UPDATED_PRICE_MICROS, UNITED_STATES_CURRENCY_CODE));
add(new BuyableProductPageAnnotation(UPDATED_PRICE_MICROS,
UNITED_STATES_CURRENCY_CODE, FAKE_OFFER_ID));
break;
case MockPageAnnotationsResponse.BUYABLE_PRODUCT_AND_PRODUCT_UPDATE:
add(new BuyableProductPageAnnotation(
PRICE_MICROS, UNITED_STATES_CURRENCY_CODE));
PRICE_MICROS, UNITED_STATES_CURRENCY_CODE, FAKE_OFFER_ID));
add(new ProductPriceUpdatePageAnnotation(PRICE_MICROS,
UPDATED_PRICE_MICROS, UNITED_STATES_CURRENCY_CODE));
break;
Expand Down
4 changes: 4 additions & 0 deletions chrome/browser/commerce/subscriptions/android/BUILD.gn
Expand Up @@ -7,6 +7,9 @@ import("//build/config/android/rules.gni")
android_library("java") {
sources = [
"java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscription.java",
"java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionJsonSerializer.java",
"java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceConfig.java",
"java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceProxy.java",
"java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsStorage.java",
"java/src/org/chromium/chrome/browser/subscriptions/ImplicitPriceDropSubscriptionsManager.java",
"java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManager.java",
Expand All @@ -17,6 +20,7 @@ android_library("java") {
"//base:base_java",
"//chrome/android:base_module_java",
"//chrome/browser/android/lifecycle:java",
"//chrome/browser/endpoint_fetcher:java",
"//chrome/browser/flags:java",
"//chrome/browser/preferences:java",
"//chrome/browser/profiles/android:java",
Expand Down
Expand Up @@ -41,7 +41,7 @@ public class CommerceSubscription {
String OFFER_ID = "OFFER_ID";
}

private static final long UNSAVED_SUBSCRIPTION = -1L;
public static final long UNSAVED_SUBSCRIPTION = -1L;

private final long mTimestamp;
@NonNull
Expand Down
@@ -0,0 +1,64 @@
// Copyright 2021 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.subscriptions;

import org.json.JSONException;
import org.json.JSONObject;

import org.chromium.base.Log;

import java.util.Locale;

/**
* Helpers for serializing and deserializing {@link CommerceSubscription} objects.
*/
class CommerceSubscriptionJsonSerializer {
private static final String TAG = "CSJS";
private static final String SUBSCRIPTION_TYPE_KEY = "type";
private static final String SUBSCRIPTION_IDENTIFIER_KEY = "identifier";
private static final String SUBSCRIPTION_IDENTIFIER_TYPE_KEY = "identifierType";
private static final String SUBSCRIPTION_MANAGEMENT_TYPE_KEY = "managementType";
private static final String SUBSCRIPTION_TIMESTAMP_KEY = "eventTimestampMicros";

/** Creates a {@link CommerceSubscription} from a {@link JSONObject}. */
public static CommerceSubscription deserialize(JSONObject json) {
try {
return new CommerceSubscription(json.getString(SUBSCRIPTION_TYPE_KEY),
json.getString(SUBSCRIPTION_IDENTIFIER_KEY),
json.getString(SUBSCRIPTION_MANAGEMENT_TYPE_KEY),
json.getString(SUBSCRIPTION_IDENTIFIER_TYPE_KEY),
Long.parseLong(json.getString(SUBSCRIPTION_TIMESTAMP_KEY)));

} catch (JSONException e) {
Log.e(TAG,
String.format(Locale.US,
"Failed to deserialize CommerceSubscription. Details: %s",
e.getMessage()));
}
return null;
}

/** Creates a {@link JSONObject}from a {@link CommerceSubscription}. */
public static JSONObject serialize(CommerceSubscription subscription) {
try {
JSONObject subscriptionJson = new JSONObject();
subscriptionJson.put(SUBSCRIPTION_TYPE_KEY, subscription.getType());
subscriptionJson.put(
SUBSCRIPTION_MANAGEMENT_TYPE_KEY, subscription.getManagementType());
subscriptionJson.put(
SUBSCRIPTION_IDENTIFIER_TYPE_KEY, subscription.getTrackingIdType());
subscriptionJson.put(SUBSCRIPTION_IDENTIFIER_KEY, subscription.getTrackingId());

return subscriptionJson;
} catch (JSONException e) {
Log.e(TAG,
String.format(Locale.US,
"Failed to serialize CommerceSubscription. Details: %s",
e.getMessage()));
}

return null;
}
}
@@ -0,0 +1,29 @@
// Copyright 2021 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.subscriptions;

import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.IntCachedFieldTrialParameter;
import org.chromium.chrome.browser.flags.StringCachedFieldTrialParameter;

import java.util.concurrent.TimeUnit;

/** Flag configuration for Commerce Subscriptions Service. */
public class CommerceSubscriptionsServiceConfig {
private static final String BASE_URL_PARAM = "subscriptions_service_base_url";
private static final String DEFAULT_BASE_URL =
"https://memex-pa.googleapis.com/v1/shopping/subscriptions";

private static final String STALE_TAB_LOWER_BOUND_SECONDS_PARAM =
"price_tracking_stale_tab_lower_bound_seconds";

public static final StringCachedFieldTrialParameter SUBSCRIPTIONS_SERVICE_BASE_URL =
new StringCachedFieldTrialParameter(
ChromeFeatureList.TAB_GRID_LAYOUT_ANDROID, BASE_URL_PARAM, DEFAULT_BASE_URL);

public static final IntCachedFieldTrialParameter STALE_TAB_LOWER_BOUND_SECONDS =
new IntCachedFieldTrialParameter(ChromeFeatureList.TAB_GRID_LAYOUT_ANDROID,
STALE_TAB_LOWER_BOUND_SECONDS_PARAM, (int) TimeUnit.DAYS.toSeconds(1));
}
@@ -0,0 +1,184 @@
// Copyright 2021 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.subscriptions;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import org.chromium.base.Callback;
import org.chromium.base.Log;
import org.chromium.chrome.browser.endpoint_fetcher.EndpointFetcher;
import org.chromium.chrome.browser.profiles.Profile;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/**
* Wrapper around CommerceSubscriptions Web APIs.
*/
public final class CommerceSubscriptionsServiceProxy {
private static final String TAG = "CSSP";
private static final long HTTPS_REQUEST_TIMEOUT_MS = 1000L;
private static final String GET_HTTPS_METHOD = "GET";
private static final String POST_HTTPS_METHOD = "POST";
private static final String CONTENT_TYPE = "application/json; charset=UTF-8";
private static final String EMPTY_POST_DATA = "";
private static final String[] OAUTH_SCOPE =
new String[] {"https://www.googleapis.com/auth/chromememex"};
private static final String OAUTH_NAME = "susbcriptions_svc";
private static final String STATUS_KEY = "status";
private static final String STATUS_CODE_KEY = "code";
private static final String REMOVE_SUBSCRIPTIONS_REQUEST_PARAMS_KEY =
"removeShoppingSubscriptionsParams";
private static final String CREATE_SUBSCRIPTIONS_REQUEST_PARAMS_KEY =
"createShoppingSubscriptionsParams";
private static final String EVENT_TIMESTAMP_MICROS_KEY = "eventTimestampMicros";
private static final String SUBSCRIPTIONS_KEY = "subscriptions";
private static final String GET_SUBSCRIPTIONS_QUERY_PARAMS_TEMPLATE =
"?requestParams.subscriptionType=%s";
private static final int BACKEND_CANONICAL_CODE_SUCCESS = 0;

/**
* Makes an HTTPS call to the backend in order to create the provided subscriptions.
* @param subscriptions list of {@link CommerceSubscription} to create.
* @param callback indicates whether or not the operation succeeded on the backend.
*/
public void create(List<CommerceSubscription> subscriptions, Callback<Boolean> callback) {
manageSubscriptions(getCreateSubscriptionsRequestParams(subscriptions), callback);
}

/**
* Makes an HTTPS call to the backend to delete the provided list of subscriptions.
* @param subscriptions list of {@link CommerceSubscription} to delete.
* @param callback indicates whether or not the operation succeeded on the backend.
*/
public void delete(List<CommerceSubscription> subscriptions, Callback<Boolean> callback) {
manageSubscriptions(getRemoveSubscriptionsRequestParams(subscriptions), callback);
}

/**
* Fetches all subscriptions that match the provided type from the backend.
* @param type the type of subscriptions to fetch.
* @param callback contains the list of subscriptions returned from the server.
*/
public void get(@CommerceSubscription.CommerceSubscriptionType String type,
Callback<List<CommerceSubscription>> callback) {
// TODO(crbug.com/1195469) Accept Profile instance from SubscriptionsManager.
EndpointFetcher.fetchUsingOAuth(
(response)
-> {
callback.onResult(createCommerceSubscriptions(response.getResponseString()));
},
Profile.getLastUsedRegularProfile(), OAUTH_NAME,
CommerceSubscriptionsServiceConfig.SUBSCRIPTIONS_SERVICE_BASE_URL.getValue()
+ String.format(GET_SUBSCRIPTIONS_QUERY_PARAMS_TEMPLATE, type),
GET_HTTPS_METHOD, CONTENT_TYPE, OAUTH_SCOPE, EMPTY_POST_DATA,
HTTPS_REQUEST_TIMEOUT_MS);
}

private void manageSubscriptions(JSONObject requestPayload, Callback<Boolean> callback) {
// TODO(crbug.com/1195469) Accept Profile instance from SubscriptionsManager.
EndpointFetcher.fetchUsingOAuth(
(response)
-> {
callback.onResult(
didManageSubscriptionCallSucceed(response.getResponseString()));
},
Profile.getLastUsedRegularProfile(), OAUTH_NAME,
CommerceSubscriptionsServiceConfig.SUBSCRIPTIONS_SERVICE_BASE_URL.getValue(),
POST_HTTPS_METHOD, CONTENT_TYPE, OAUTH_SCOPE, requestPayload.toString(),
HTTPS_REQUEST_TIMEOUT_MS);
}

private boolean didManageSubscriptionCallSucceed(String responseString) {
try {
JSONObject response = new JSONObject(responseString);
JSONObject statusJson = response.getJSONObject(STATUS_KEY);
int statusCode = statusJson.getInt(STATUS_CODE_KEY);
return statusCode == BACKEND_CANONICAL_CODE_SUCCESS;
} catch (JSONException e) {
Log.e(TAG,
String.format(Locale.US,
"Failed to create CreateSubscriptionRequestParams. Details: %s",
e.getMessage()));
}

return false;
}

private JSONObject getCreateSubscriptionsRequestParams(
List<CommerceSubscription> subscriptions) {
JSONObject container = new JSONObject();
JSONArray subscriptionsJsonArray = new JSONArray();
try {
for (CommerceSubscription subscription : subscriptions) {
subscriptionsJsonArray.put(
CommerceSubscriptionJsonSerializer.serialize(subscription));
}

JSONObject subscriptionsObject = new JSONObject();
subscriptionsObject.put(SUBSCRIPTIONS_KEY, subscriptionsJsonArray);

container.put(CREATE_SUBSCRIPTIONS_REQUEST_PARAMS_KEY, subscriptionsObject);
} catch (JSONException e) {
Log.e(TAG,
String.format(Locale.US,
"Failed to create CreateSubscriptionRequestParams. Details: %s",
e.getMessage()));
}

return container;
}

private JSONObject getRemoveSubscriptionsRequestParams(
List<CommerceSubscription> subscriptions) {
JSONObject container = new JSONObject();
try {
JSONObject removeSubscriptionsParamsJson = new JSONObject();
JSONArray subscriptionsTimestamps = new JSONArray();
for (CommerceSubscription subscription : subscriptions) {
if (subscription.getTimestamp() == CommerceSubscription.UNSAVED_SUBSCRIPTION) {
continue;
}
subscriptionsTimestamps.put(subscription.getTimestamp());
}
removeSubscriptionsParamsJson.put(EVENT_TIMESTAMP_MICROS_KEY, subscriptionsTimestamps);
container.put(REMOVE_SUBSCRIPTIONS_REQUEST_PARAMS_KEY, removeSubscriptionsParamsJson);
} catch (JSONException e) {
Log.e(TAG,
String.format(Locale.US,
"Failed to create RemoveSubscriptionsRequestParams. Details: %s",
e.getMessage()));
}

return container;
}

private List<CommerceSubscription> createCommerceSubscriptions(String responseString) {
List<CommerceSubscription> subscriptions = new ArrayList<>();
try {
JSONObject response = new JSONObject(responseString);
JSONArray subscriptionsJsonArray = response.getJSONArray(SUBSCRIPTIONS_KEY);

for (int i = 0; i < subscriptionsJsonArray.length(); i++) {
JSONObject subscriptionJson = subscriptionsJsonArray.getJSONObject(i);
CommerceSubscription subscription =
CommerceSubscriptionJsonSerializer.deserialize(subscriptionJson);
if (subscription != null) {
subscriptions.add(subscription);
}
}
} catch (JSONException e) {
Log.e(TAG,
String.format(Locale.US,
"Failed to deserialize Subscriptions list. Details: %s",
e.getMessage()));
}

return subscriptions;
}
}

0 comments on commit 37c1f35

Please sign in to comment.