Commit a04f522e authored by Peter E Conn's avatar Peter E Conn Committed by Commit Bot

💸 Implement Digital Goods API ListPurchases on Android.

This CL takes the Mojo changes from:
  https://chromium-review.googlesource.com/c/chromium/src/+/2484038
and adds an Android implementation for them.

Once these mojo changes are wired up on the Blink side, I'll add some
tests to DigitalGoodsTests.

Bug: 1139795
Change-Id: Ib8c92cbc619b8977f37d3a9b59a3045d5c1bbb7f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2510271
Commit-Queue: Peter Conn <peconn@chromium.org>
Reviewed-by: default avatarMichael van Ouwerkerk <mvanouwerkerk@chromium.org>
Reviewed-by: default avatarDominick Ng <dominickn@chromium.org>
Reviewed-by: default avatarGlen Robertson <glenrob@chromium.org>
Reviewed-by: default avatarAndrew Grieve <agrieve@chromium.org>
Cr-Commit-Position: refs/heads/master@{#824001}
parent 9ba7081c
...@@ -900,6 +900,7 @@ junit_binary("chrome_junit_tests") { ...@@ -900,6 +900,7 @@ junit_binary("chrome_junit_tests") {
"//content/public/test/android:content_java_test_support", "//content/public/test/android:content_java_test_support",
"//mojo/public/java:bindings_java", "//mojo/public/java:bindings_java",
"//mojo/public/java:system_java", "//mojo/public/java:system_java",
"//mojo/public/mojom/base:base_java",
"//net/android:net_java", "//net/android:net_java",
"//services/device/public/mojom:mojom_java", "//services/device/public/mojom:mojom_java",
"//services/media_session/public/cpp/android:media_session_java", "//services/media_session/public/cpp/android:media_session_java",
......
...@@ -191,6 +191,7 @@ chrome_java_sources = [ ...@@ -191,6 +191,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/browserservices/digitalgoods/DigitalGoodsFactoryImpl.java", "java/src/org/chromium/chrome/browser/browserservices/digitalgoods/DigitalGoodsFactoryImpl.java",
"java/src/org/chromium/chrome/browser/browserservices/digitalgoods/DigitalGoodsImpl.java", "java/src/org/chromium/chrome/browser/browserservices/digitalgoods/DigitalGoodsImpl.java",
"java/src/org/chromium/chrome/browser/browserservices/digitalgoods/GetDetailsConverter.java", "java/src/org/chromium/chrome/browser/browserservices/digitalgoods/GetDetailsConverter.java",
"java/src/org/chromium/chrome/browser/browserservices/digitalgoods/ListPurchasesConverter.java",
"java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappBridge.java", "java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappBridge.java",
"java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappGeolocationBridge.java", "java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappGeolocationBridge.java",
"java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/LocationPermissionUpdater.java", "java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/LocationPermissionUpdater.java",
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
package org.chromium.chrome.browser.browserservices.digitalgoods; package org.chromium.chrome.browser.browserservices.digitalgoods;
import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGoodsConverter.convertResponseCodes; import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGoodsConverter.convertResponseCode;
import android.os.Bundle; import android.os.Bundle;
...@@ -60,7 +60,7 @@ class AcknowledgeConverter { ...@@ -60,7 +60,7 @@ class AcknowledgeConverter {
} }
int code = args.getInt(RESPONSE_ACKNOWLEDGE_RESPONSE_CODE); int code = args.getInt(RESPONSE_ACKNOWLEDGE_RESPONSE_CODE);
callback.call(convertResponseCodes(code)); callback.call(convertResponseCode(code));
} }
}; };
} }
......
...@@ -15,6 +15,7 @@ import org.chromium.chrome.browser.browserservices.TrustedWebActivityClient; ...@@ -15,6 +15,7 @@ import org.chromium.chrome.browser.browserservices.TrustedWebActivityClient;
import org.chromium.components.embedder_support.util.Origin; import org.chromium.components.embedder_support.util.Origin;
import org.chromium.payments.mojom.DigitalGoods.AcknowledgeResponse; import org.chromium.payments.mojom.DigitalGoods.AcknowledgeResponse;
import org.chromium.payments.mojom.DigitalGoods.GetDetailsResponse; import org.chromium.payments.mojom.DigitalGoods.GetDetailsResponse;
import org.chromium.payments.mojom.DigitalGoods.ListPurchasesResponse;
/** /**
* This class uses the {@link DigitalGoodsConverter} to convert data types between mojo types and * This class uses the {@link DigitalGoodsConverter} to convert data types between mojo types and
...@@ -26,6 +27,7 @@ public class DigitalGoodsAdapter { ...@@ -26,6 +27,7 @@ public class DigitalGoodsAdapter {
public static final String COMMAND_ACKNOWLEDGE = "acknowledge"; public static final String COMMAND_ACKNOWLEDGE = "acknowledge";
public static final String COMMAND_GET_DETAILS = "getDetails"; public static final String COMMAND_GET_DETAILS = "getDetails";
public static final String COMMAND_LIST_PURCHASES = "listPurchases";
public static final String KEY_SUCCESS = "success"; public static final String KEY_SUCCESS = "success";
private final TrustedWebActivityClient mClient; private final TrustedWebActivityClient mClient;
...@@ -53,6 +55,15 @@ public class DigitalGoodsAdapter { ...@@ -53,6 +55,15 @@ public class DigitalGoodsAdapter {
execute(scope, COMMAND_ACKNOWLEDGE, args, callback, onError, onUnavailable); execute(scope, COMMAND_ACKNOWLEDGE, args, callback, onError, onUnavailable);
} }
public void listPurchases(Uri scope, ListPurchasesResponse response) {
Bundle args = new Bundle();
TrustedWebActivityCallback callback = ListPurchasesConverter.convertCallback(response);
Runnable onError = () -> ListPurchasesConverter.returnClientAppError(response);
Runnable onUnavailable = () -> ListPurchasesConverter.returnClientAppUnavailable(response);
execute(scope, COMMAND_LIST_PURCHASES, args, callback, onError, onUnavailable);
}
private void execute(Uri scope, String command, Bundle args, private void execute(Uri scope, String command, Bundle args,
TrustedWebActivityCallback callback, Runnable onClientAppError, TrustedWebActivityCallback callback, Runnable onClientAppError,
Runnable onClientAppUnavailable) { Runnable onClientAppUnavailable) {
......
...@@ -4,9 +4,17 @@ ...@@ -4,9 +4,17 @@
package org.chromium.chrome.browser.browserservices.digitalgoods; package org.chromium.chrome.browser.browserservices.digitalgoods;
import android.os.Bundle;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.payments.mojom.BillingResponseCode; import org.chromium.payments.mojom.BillingResponseCode;
import java.util.ArrayList;
import java.util.List;
/** /**
* The *Converter classes take care of converting between the mojo types that * The *Converter classes take care of converting between the mojo types that
* {@link DigitalGoodsImpl} deals with and the Android types that {@link TrustedWebActivityClient} * {@link DigitalGoodsImpl} deals with and the Android types that {@link TrustedWebActivityClient}
...@@ -27,7 +35,7 @@ public class DigitalGoodsConverter { ...@@ -27,7 +35,7 @@ public class DigitalGoodsConverter {
private DigitalGoodsConverter() {} private DigitalGoodsConverter() {}
static int convertResponseCodes(int responseCode) { static int convertResponseCode(int responseCode) {
switch (responseCode) { switch (responseCode) {
case PLAY_BILLING_OK: case PLAY_BILLING_OK:
return BillingResponseCode.OK; return BillingResponseCode.OK;
...@@ -42,4 +50,38 @@ public class DigitalGoodsConverter { ...@@ -42,4 +50,38 @@ public class DigitalGoodsConverter {
return BillingResponseCode.ERROR; return BillingResponseCode.ERROR;
} }
} }
/** Checks that the given field exists and is of the required type in a Bundle. */
static <T> boolean checkField(Bundle bundle, String key, Class<T> clazz) {
if (bundle.containsKey(key) && clazz.isAssignableFrom(bundle.get(key).getClass())) {
return true;
}
Log.w(TAG, "Missing field " + key + " of type " + clazz.getName() + ".");
return false;
}
/** An interface for use with {@link #convertParcelableArray}. */
interface Converter<T> {
@Nullable
T convert(Bundle bundle);
}
/**
* Runs through the given {@link Parcelable[]} and for each item that is a {@link Bundle}, calls
* {@link Converter#convert}, returning an array of the non-null results.
*/
static <T> List<T> convertParcelableArray(Parcelable[] array, Converter<T> converter) {
List<T> list = new ArrayList<>();
for (Parcelable item : array) {
if (!(item instanceof Bundle)) {
Log.w(TAG, "Passed a Parcelable that was not a Bundle.");
continue;
}
T converted = converter.convert((Bundle) item);
if (converted != null) list.add(converted);
}
return list;
}
} }
...@@ -12,6 +12,7 @@ import org.chromium.mojo.system.MojoException; ...@@ -12,6 +12,7 @@ import org.chromium.mojo.system.MojoException;
import org.chromium.payments.mojom.DigitalGoods; import org.chromium.payments.mojom.DigitalGoods;
import org.chromium.payments.mojom.DigitalGoods.AcknowledgeResponse; import org.chromium.payments.mojom.DigitalGoods.AcknowledgeResponse;
import org.chromium.payments.mojom.DigitalGoods.GetDetailsResponse; import org.chromium.payments.mojom.DigitalGoods.GetDetailsResponse;
import org.chromium.payments.mojom.DigitalGoods.ListPurchasesResponse;
/** /**
* An implementation of the {@link DigitalGoods} mojo interface that communicates with Trusted Web * An implementation of the {@link DigitalGoods} mojo interface that communicates with Trusted Web
...@@ -49,6 +50,12 @@ public class DigitalGoodsImpl implements DigitalGoods { ...@@ -49,6 +50,12 @@ public class DigitalGoodsImpl implements DigitalGoods {
} }
} }
@Override
public void listPurchases(ListPurchasesResponse callback) {
String url = mDelegate.getUrl();
if (url != null) mAdapter.listPurchases(Uri.parse(mDelegate.getUrl()), callback);
}
@Override @Override
public void close() {} public void close() {}
......
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
package org.chromium.chrome.browser.browserservices.digitalgoods; package org.chromium.chrome.browser.browserservices.digitalgoods;
import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGoodsConverter.convertResponseCodes; import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGoodsConverter.checkField;
import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGoodsConverter.convertParcelableArray;
import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGoodsConverter.convertResponseCode;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
...@@ -20,9 +22,6 @@ import org.chromium.payments.mojom.DigitalGoods.GetDetailsResponse; ...@@ -20,9 +22,6 @@ import org.chromium.payments.mojom.DigitalGoods.GetDetailsResponse;
import org.chromium.payments.mojom.ItemDetails; import org.chromium.payments.mojom.ItemDetails;
import org.chromium.payments.mojom.PaymentCurrencyAmount; import org.chromium.payments.mojom.PaymentCurrencyAmount;
import java.util.ArrayList;
import java.util.List;
/** /**
* A converter that deals with the parameters and result for GetDetails calls. * A converter that deals with the parameters and result for GetDetails calls.
*/ */
...@@ -30,9 +29,9 @@ public class GetDetailsConverter { ...@@ -30,9 +29,9 @@ public class GetDetailsConverter {
private static final String TAG = "DigitalGoods"; private static final String TAG = "DigitalGoods";
static final String PARAM_GET_DETAILS_ITEM_IDS = "getDetails.itemIds"; static final String PARAM_GET_DETAILS_ITEM_IDS = "getDetails.itemIds";
public static final String RESPONSE_GET_DETAILS = "getDetails.response"; public static final String RESPONSE_COMMAND = "getDetails.response";
static final String RESPONSE_GET_DETAILS_RESPONSE_CODE = "getDetails.responseCode"; static final String KEY_RESPONSE_CODE = "getDetails.responseCode";
static final String RESPONSE_GET_DETAILS_DETAILS_LIST = "getDetails.detailsList"; static final String KEY_DETAILS_LIST = "getDetails.detailsList";
static final String KEY_ID = "itemDetails.id"; static final String KEY_ID = "itemDetails.id";
static final String KEY_TITLE = "itemDetails.title"; static final String KEY_TITLE = "itemDetails.title";
...@@ -66,7 +65,7 @@ public class GetDetailsConverter { ...@@ -66,7 +65,7 @@ public class GetDetailsConverter {
return new TrustedWebActivityCallback() { return new TrustedWebActivityCallback() {
@Override @Override
public void onExtraCallback(@NonNull String callbackName, @Nullable Bundle args) { public void onExtraCallback(@NonNull String callbackName, @Nullable Bundle args) {
if (!RESPONSE_GET_DETAILS.equals(callbackName)) { if (!RESPONSE_COMMAND.equals(callbackName)) {
Log.w(TAG, "Wrong callback name given: " + callbackName + "."); Log.w(TAG, "Wrong callback name given: " + callbackName + ".");
returnClientAppError(callback); returnClientAppError(callback);
return; return;
...@@ -78,39 +77,26 @@ public class GetDetailsConverter { ...@@ -78,39 +77,26 @@ public class GetDetailsConverter {
return; return;
} }
if (!(args.get(RESPONSE_GET_DETAILS_RESPONSE_CODE) instanceof Integer) if (!checkField(args, KEY_RESPONSE_CODE, Integer.class)
|| !(args.get(RESPONSE_GET_DETAILS_DETAILS_LIST) instanceof Parcelable[])) { || !checkField(args, KEY_DETAILS_LIST, Parcelable[].class)) {
Log.w(TAG, "Poorly formed args provided.");
returnClientAppError(callback); returnClientAppError(callback);
return; return;
} }
int code = args.getInt(RESPONSE_GET_DETAILS_RESPONSE_CODE); int code = args.getInt(KEY_RESPONSE_CODE);
ItemDetails[] details = convertItemDetailsList( Parcelable[] array = args.getParcelableArray(KEY_DETAILS_LIST);
args.getParcelableArray(RESPONSE_GET_DETAILS_DETAILS_LIST));
callback.call(convertResponseCodes(code), details);
}
};
}
private static ItemDetails[] convertItemDetailsList(Parcelable[] list) { ItemDetails[] details =
List<ItemDetails> details = new ArrayList<>(); convertParcelableArray(array, GetDetailsConverter::convertItemDetails)
for (Parcelable item : list) { .toArray(new ItemDetails[0]);
details.add(convertItemDetails(item)); callback.call(convertResponseCode(code), details);
} }
return details.toArray(new ItemDetails[0]); };
} }
/** Extracts an {@link ItemDetails} from a {@link Parcelable}. */ /** Extracts an {@link ItemDetails} from a {@link Parcelable}. */
@Nullable @Nullable
static ItemDetails convertItemDetails(Parcelable itemAsParcelable) { static ItemDetails convertItemDetails(Bundle item) {
if (!(itemAsParcelable instanceof Bundle)) {
Log.w(TAG, "Item is not a Bundle.");
return null;
}
Bundle item = (Bundle) itemAsParcelable;
for (String field : REQUIRED_FIELDS) { for (String field : REQUIRED_FIELDS) {
if (item.containsKey(field) && (item.get(field) instanceof String)) continue; if (item.containsKey(field) && (item.get(field) instanceof String)) continue;
Log.w(TAG, "Item does not contain field String " + field + "."); Log.w(TAG, "Item does not contain field String " + field + ".");
...@@ -146,11 +132,11 @@ public class GetDetailsConverter { ...@@ -146,11 +132,11 @@ public class GetDetailsConverter {
return result; return result;
} }
public static void returnClientAppUnavailable(GetDetailsResponse callback) { static void returnClientAppUnavailable(GetDetailsResponse callback) {
callback.call(BillingResponseCode.CLIENT_APP_UNAVAILABLE, new ItemDetails[0]); callback.call(BillingResponseCode.CLIENT_APP_UNAVAILABLE, new ItemDetails[0]);
} }
public static void returnClientAppError(GetDetailsResponse callback) { static void returnClientAppError(GetDetailsResponse callback) {
callback.call(BillingResponseCode.CLIENT_APP_ERROR, new ItemDetails[0]); callback.call(BillingResponseCode.CLIENT_APP_ERROR, new ItemDetails[0]);
} }
...@@ -159,10 +145,10 @@ public class GetDetailsConverter { ...@@ -159,10 +145,10 @@ public class GetDetailsConverter {
* This would be used by the client app and is here only to help testing. * This would be used by the client app and is here only to help testing.
*/ */
@VisibleForTesting @VisibleForTesting
public static Bundle createItemDetailsBundle(String id, String title, String desc, static Bundle createItemDetailsBundle(String id, String title, String desc, String currency,
String currency, String value, @Nullable String subsPeriod, String value, @Nullable String subsPeriod, @Nullable String freeTrialPeriod,
@Nullable String freeTrialPeriod, @Nullable String introPriceCurrency, @Nullable String introPriceCurrency, @Nullable String introPriceValue,
@Nullable String introPriceValue, @Nullable String intoPricePeriod) { @Nullable String intoPricePeriod) {
Bundle bundle = createItemDetailsBundle(id, title, desc, currency, value); Bundle bundle = createItemDetailsBundle(id, title, desc, currency, value);
bundle.putString(KEY_SUBS_PERIOD, subsPeriod); bundle.putString(KEY_SUBS_PERIOD, subsPeriod);
...@@ -178,7 +164,7 @@ public class GetDetailsConverter { ...@@ -178,7 +164,7 @@ public class GetDetailsConverter {
* Like the above method, but provides {@code null} for all optional parameters. * Like the above method, but provides {@code null} for all optional parameters.
*/ */
@VisibleForTesting @VisibleForTesting
public static Bundle createItemDetailsBundle( static Bundle createItemDetailsBundle(
String id, String title, String desc, String currency, String value) { String id, String title, String desc, String currency, String value) {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
...@@ -196,11 +182,11 @@ public class GetDetailsConverter { ...@@ -196,11 +182,11 @@ public class GetDetailsConverter {
* carried out by the client app and is only here to help testing. * carried out by the client app and is only here to help testing.
*/ */
@VisibleForTesting @VisibleForTesting
public static Bundle createResponseBundle(int responseCode, Bundle... itemDetails) { static Bundle createResponseBundle(int responseCode, Bundle... itemDetails) {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putInt(RESPONSE_GET_DETAILS_RESPONSE_CODE, responseCode); bundle.putInt(KEY_RESPONSE_CODE, responseCode);
bundle.putParcelableArray(RESPONSE_GET_DETAILS_DETAILS_LIST, itemDetails); bundle.putParcelableArray(KEY_DETAILS_LIST, itemDetails);
return bundle; return bundle;
} }
......
// 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.browserservices.digitalgoods;
import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGoodsConverter.checkField;
import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGoodsConverter.convertParcelableArray;
import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGoodsConverter.convertResponseCode;
import android.os.Bundle;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.browser.trusted.TrustedWebActivityCallback;
import org.chromium.base.Log;
import org.chromium.mojo_base.mojom.TimeDelta;
import org.chromium.payments.mojom.BillingResponseCode;
import org.chromium.payments.mojom.DigitalGoods.ListPurchasesResponse;
import org.chromium.payments.mojom.PurchaseDetails;
import org.chromium.payments.mojom.PurchaseState;
/**
* A converter that deals with the results of ListPurchases calls.
*/
class ListPurchasesConverter {
private static final String TAG = "DigitalGoods";
static final String RESPONSE_COMMAND = "listPurchases.response";
static final String KEY_PURCHASES_LIST = "listPurchases.purchasesList";
static final String KEY_RESPONSE_CODE = "listPurchases.responseCode";
static final String KEY_ITEM_ID = "listPurchases.itemId";
static final String KEY_PURCHASE_TOKEN = "listPurchases.purchaseToken";
static final String KEY_ACKNOWLEDGED = "listPurchases.acknowledged";
static final String KEY_PURCHASE_STATE = "listPurchases.purchaseState";
static final String KEY_PURCHASE_TIME_MS_PAST_UNIX_EPOCH = "listPurchases.purchaseTime";
static final String KEY_WILL_AUTO_RENEW = "listPurchases.willAutoRenew";
// These values are copied from the Play Billing library since Chrome cannot depend on it.
// https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState
static final int PLAY_BILLING_PURCHASE_STATE_PENDING = 2;
static final int PLAY_BILLING_PURCHASE_STATE_PURCHASED = 1;
static final int PLAY_BILLING_PURCHASE_STATE_UNSPECIFIED = 0;
/**
* Produces a {@link TrustedWebActivityCallback} that calls the given
* {@link ListPurchasesResponse}.
*/
static TrustedWebActivityCallback convertCallback(ListPurchasesResponse callback) {
return new TrustedWebActivityCallback() {
@Override
public void onExtraCallback(@NonNull String callbackName, @Nullable Bundle args) {
if (!RESPONSE_COMMAND.equals(callbackName)) {
Log.w(TAG, "Wrong callback name given: " + callbackName + ".");
returnClientAppError(callback);
return;
}
if (args == null) {
Log.w(TAG, "No args provided.");
returnClientAppError(callback);
return;
}
if (!checkField(args, KEY_RESPONSE_CODE, Integer.class)
|| !checkField(args, KEY_PURCHASES_LIST, Parcelable[].class)) {
returnClientAppError(callback);
return;
}
int code = args.getInt(KEY_RESPONSE_CODE);
Parcelable[] array = args.getParcelableArray(KEY_PURCHASES_LIST);
PurchaseDetails[] details = convertParcelableArray(
array, ListPurchasesConverter::convertPurchaseDetails)
.toArray(new PurchaseDetails[0]);
callback.call(convertResponseCode(code), details);
}
};
}
static PurchaseDetails convertPurchaseDetails(Bundle purchase) {
if (!checkField(purchase, KEY_ITEM_ID, String.class)) return null;
if (!checkField(purchase, KEY_PURCHASE_TOKEN, String.class)) return null;
if (!checkField(purchase, KEY_ACKNOWLEDGED, Boolean.class)) return null;
if (!checkField(purchase, KEY_PURCHASE_STATE, Integer.class)) return null;
if (!checkField(purchase, KEY_PURCHASE_TIME_MS_PAST_UNIX_EPOCH, Long.class)) return null;
if (!checkField(purchase, KEY_WILL_AUTO_RENEW, Boolean.class)) return null;
TimeDelta purchaseTime = new TimeDelta();
purchaseTime.microseconds = purchase.getLong(KEY_PURCHASE_TIME_MS_PAST_UNIX_EPOCH);
PurchaseDetails result = new PurchaseDetails();
result.itemId = purchase.getString(KEY_ITEM_ID);
result.purchaseToken = purchase.getString(KEY_PURCHASE_TOKEN);
result.acknowledged = purchase.getBoolean(KEY_ACKNOWLEDGED);
result.purchaseState = convertPurchaseState(purchase.getInt(KEY_PURCHASE_STATE));
result.purchaseTime = purchaseTime;
result.willAutoRenew = purchase.getBoolean(KEY_WILL_AUTO_RENEW);
return result;
}
static int convertPurchaseState(int purchaseState) {
switch (purchaseState) {
case PLAY_BILLING_PURCHASE_STATE_PENDING:
return PurchaseState.PENDING;
case PLAY_BILLING_PURCHASE_STATE_PURCHASED:
return PurchaseState.PURCHASED;
default:
return PurchaseState.UNKNOWN;
}
}
static void returnClientAppUnavailable(ListPurchasesResponse callback) {
callback.call(BillingResponseCode.CLIENT_APP_UNAVAILABLE, new PurchaseDetails[0]);
}
static void returnClientAppError(ListPurchasesResponse callback) {
callback.call(BillingResponseCode.CLIENT_APP_ERROR, new PurchaseDetails[0]);
}
@VisibleForTesting
static Bundle createPurchaseDetailsBundle(String itemId, String purchaseToken,
boolean acknowledged, int purchaseState, long purchaseTimeMsPastUnixEpoch,
boolean willAutoRenew) {
Bundle bundle = new Bundle();
bundle.putString(KEY_ITEM_ID, itemId);
bundle.putString(KEY_PURCHASE_TOKEN, purchaseToken);
bundle.putBoolean(KEY_ACKNOWLEDGED, acknowledged);
bundle.putInt(KEY_PURCHASE_STATE, purchaseState);
bundle.putLong(KEY_PURCHASE_TIME_MS_PAST_UNIX_EPOCH, purchaseTimeMsPastUnixEpoch);
bundle.putBoolean(KEY_WILL_AUTO_RENEW, willAutoRenew);
return bundle;
}
@VisibleForTesting
static Bundle createResponseBundle(int responseCode, Bundle... purchaseDetails) {
Bundle bundle = new Bundle();
bundle.putInt(KEY_RESPONSE_CODE, responseCode);
bundle.putParcelableArray(KEY_PURCHASES_LIST, purchaseDetails);
return bundle;
}
}
...@@ -10,7 +10,6 @@ import static org.chromium.chrome.browser.browserservices.TestTrustedWebActivity ...@@ -10,7 +10,6 @@ import static org.chromium.chrome.browser.browserservices.TestTrustedWebActivity
import static org.chromium.chrome.browser.browserservices.TestTrustedWebActivityService.SET_RESPONSE_BUNDLE; import static org.chromium.chrome.browser.browserservices.TestTrustedWebActivityService.SET_RESPONSE_BUNDLE;
import static org.chromium.chrome.browser.browserservices.TestTrustedWebActivityService.SET_RESPONSE_NAME; import static org.chromium.chrome.browser.browserservices.TestTrustedWebActivityService.SET_RESPONSE_NAME;
import static org.chromium.chrome.browser.browserservices.digitalgoods.AcknowledgeConverter.RESPONSE_ACKNOWLEDGE; import static org.chromium.chrome.browser.browserservices.digitalgoods.AcknowledgeConverter.RESPONSE_ACKNOWLEDGE;
import static org.chromium.chrome.browser.browserservices.digitalgoods.GetDetailsConverter.RESPONSE_GET_DETAILS;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
...@@ -115,7 +114,7 @@ public class DigitalGoodsTest { ...@@ -115,7 +114,7 @@ public class DigitalGoodsTest {
public void twaServiceConnected() throws TimeoutException { public void twaServiceConnected() throws TimeoutException {
DigitalGoodsImpl impl = createFixedDigitalGoods(); DigitalGoodsImpl impl = createFixedDigitalGoods();
setTwaServiceResponse(RESPONSE_GET_DETAILS, setTwaServiceResponse(GetDetailsConverter.RESPONSE_COMMAND,
GetDetailsConverter.createResponseBundle(0, GetDetailsConverter.createResponseBundle(0,
GetDetailsConverter.createItemDetailsBundle( GetDetailsConverter.createItemDetailsBundle(
"id1", "Item 1", "Desc 1", "GBP", "10"))); "id1", "Item 1", "Desc 1", "GBP", "10")));
...@@ -146,7 +145,7 @@ public class DigitalGoodsTest { ...@@ -146,7 +145,7 @@ public class DigitalGoodsTest {
// Note: The response code much be 0 for success otherwise it doesn't propagate through to // Note: The response code much be 0 for success otherwise it doesn't propagate through to
// JS. // JS.
setTwaServiceResponse(RESPONSE_GET_DETAILS, setTwaServiceResponse(GetDetailsConverter.RESPONSE_COMMAND,
GetDetailsConverter.createResponseBundle(0, GetDetailsConverter.createResponseBundle(0,
GetDetailsConverter.createItemDetailsBundle( GetDetailsConverter.createItemDetailsBundle(
"id1", "Item 1", "Desc 1", "GBP", "10"))); "id1", "Item 1", "Desc 1", "GBP", "10")));
......
...@@ -8,6 +8,7 @@ import org.chromium.mojo.system.MojoException; ...@@ -8,6 +8,7 @@ import org.chromium.mojo.system.MojoException;
import org.chromium.payments.mojom.DigitalGoods; import org.chromium.payments.mojom.DigitalGoods;
import org.chromium.payments.mojom.DigitalGoods.AcknowledgeResponse; import org.chromium.payments.mojom.DigitalGoods.AcknowledgeResponse;
import org.chromium.payments.mojom.DigitalGoods.GetDetailsResponse; import org.chromium.payments.mojom.DigitalGoods.GetDetailsResponse;
import org.chromium.payments.mojom.DigitalGoods.ListPurchasesResponse;
import org.chromium.payments.mojom.ItemDetails; import org.chromium.payments.mojom.ItemDetails;
import org.chromium.payments.mojom.PaymentCurrencyAmount; import org.chromium.payments.mojom.PaymentCurrencyAmount;
...@@ -53,8 +54,11 @@ class FakeDigitalGoods implements DigitalGoods { ...@@ -53,8 +54,11 @@ class FakeDigitalGoods implements DigitalGoods {
} }
@Override @Override
public void acknowledge(String purchaseToken, boolean makeAvailableAgain, public void acknowledge(
AcknowledgeResponse callback) { } String purchaseToken, boolean makeAvailableAgain, AcknowledgeResponse callback) {}
@Override
public void listPurchases(ListPurchasesResponse callback) {}
@Override @Override
public void close() {} public void close() {}
......
...@@ -6,6 +6,7 @@ package org.chromium.chrome.browser.browserservices.digitalgoods; ...@@ -6,6 +6,7 @@ package org.chromium.chrome.browser.browserservices.digitalgoods;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.chromium.chrome.browser.browserservices.digitalgoods.AcknowledgeConverter.PARAM_ACKNOWLEDGE_MAKE_AVAILABLE_AGAIN; import static org.chromium.chrome.browser.browserservices.digitalgoods.AcknowledgeConverter.PARAM_ACKNOWLEDGE_MAKE_AVAILABLE_AGAIN;
...@@ -16,13 +17,9 @@ import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGo ...@@ -16,13 +17,9 @@ import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGo
import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGoodsConverter.PLAY_BILLING_ITEM_NOT_OWNED; import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGoodsConverter.PLAY_BILLING_ITEM_NOT_OWNED;
import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGoodsConverter.PLAY_BILLING_ITEM_UNAVAILABLE; import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGoodsConverter.PLAY_BILLING_ITEM_UNAVAILABLE;
import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGoodsConverter.PLAY_BILLING_OK; import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGoodsConverter.PLAY_BILLING_OK;
import static org.chromium.chrome.browser.browserservices.digitalgoods.GetDetailsConverter.PARAM_GET_DETAILS_ITEM_IDS; import static org.chromium.chrome.browser.browserservices.digitalgoods.DigitalGoodsConverter.convertResponseCode;
import static org.chromium.chrome.browser.browserservices.digitalgoods.GetDetailsConverter.RESPONSE_GET_DETAILS;
import static org.chromium.chrome.browser.browserservices.digitalgoods.GetDetailsConverter.RESPONSE_GET_DETAILS_DETAILS_LIST;
import static org.chromium.chrome.browser.browserservices.digitalgoods.GetDetailsConverter.RESPONSE_GET_DETAILS_RESPONSE_CODE;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.browser.trusted.TrustedWebActivityCallback; import androidx.browser.trusted.TrustedWebActivityCallback;
...@@ -35,7 +32,9 @@ import org.chromium.base.test.BaseRobolectricTestRunner; ...@@ -35,7 +32,9 @@ import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.payments.mojom.BillingResponseCode; import org.chromium.payments.mojom.BillingResponseCode;
import org.chromium.payments.mojom.DigitalGoods.AcknowledgeResponse; import org.chromium.payments.mojom.DigitalGoods.AcknowledgeResponse;
import org.chromium.payments.mojom.DigitalGoods.GetDetailsResponse; import org.chromium.payments.mojom.DigitalGoods.GetDetailsResponse;
import org.chromium.payments.mojom.DigitalGoods.ListPurchasesResponse;
import org.chromium.payments.mojom.ItemDetails; import org.chromium.payments.mojom.ItemDetails;
import org.chromium.payments.mojom.PurchaseDetails;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
...@@ -53,7 +52,7 @@ public class DigitalGoodsConverterTest { ...@@ -53,7 +52,7 @@ public class DigitalGoodsConverterTest {
Bundle b = GetDetailsConverter.convertParams(itemIds); Bundle b = GetDetailsConverter.convertParams(itemIds);
String[] out = b.getStringArray(PARAM_GET_DETAILS_ITEM_IDS); String[] out = b.getStringArray(GetDetailsConverter.PARAM_GET_DETAILS_ITEM_IDS);
assertArrayEquals(itemIds, out); assertArrayEquals(itemIds, out);
} }
...@@ -91,41 +90,37 @@ public class DigitalGoodsConverterTest { ...@@ -91,41 +90,37 @@ public class DigitalGoodsConverterTest {
} }
/** /**
* A class to allow passing values out of the callback in {@link #convertGetDetailsCallback}. * A class to allow passing values out of callbacks.
*/ */
private static class TestState { private static class TestState<T> {
public int responseCode; public int responseCode;
public ItemDetails[] itemDetails; public T[] results;
} }
@Test @Test
public void convertGetDetailsCallback() { public void convertGetDetailsCallback() {
TestState state = new TestState(); TestState<ItemDetails> state = new TestState<>();
GetDetailsResponse callback = (responseCode, itemDetails) -> { GetDetailsResponse callback = (responseCode, itemDetails) -> {
state.responseCode = responseCode; state.responseCode = responseCode;
state.itemDetails = itemDetails; state.results = itemDetails;
}; };
TrustedWebActivityCallback convertedCallback = TrustedWebActivityCallback convertedCallback =
GetDetailsConverter.convertCallback(callback); GetDetailsConverter.convertCallback(callback);
Bundle args = new Bundle();
int responseCode = 0; int responseCode = 0;
Parcelable[] items = { Bundle args = GetDetailsConverter.createResponseBundle(responseCode,
GetDetailsConverter.createItemDetailsBundle("1", "t1", "d1", "c1", "v1"), GetDetailsConverter.createItemDetailsBundle("1", "t1", "d1", "c1", "v1"),
GetDetailsConverter.createItemDetailsBundle( GetDetailsConverter.createItemDetailsBundle(
"2", "t2", "d2", "c2", "v2", "sp2", "ftp2", "ipc2", "ipv2", "ipp2")}; "2", "t2", "d2", "c2", "v2", "sp2", "ftp2", "ipc2", "ipv2", "ipp2"));
args.putInt(RESPONSE_GET_DETAILS_RESPONSE_CODE, responseCode);
args.putParcelableArray(RESPONSE_GET_DETAILS_DETAILS_LIST, items);
convertedCallback.onExtraCallback(RESPONSE_GET_DETAILS, args); convertedCallback.onExtraCallback(GetDetailsConverter.RESPONSE_COMMAND, args);
assertEquals(responseCode, state.responseCode); assertEquals(convertResponseCode(responseCode), state.responseCode);
assertItemDetails(state.itemDetails[0], "1", "t1", "d1", "c1", "v1"); assertItemDetails(state.results[0], "1", "t1", "d1", "c1", "v1");
assertSubsItemDetails(state.itemDetails[0], null, null, null, null, null); assertSubsItemDetails(state.results[0], null, null, null, null, null);
assertItemDetails(state.itemDetails[1], "2", "t2", "d2", "c2", "v2"); assertItemDetails(state.results[1], "2", "t2", "d2", "c2", "v2");
assertSubsItemDetails(state.itemDetails[1], "sp2", "ftp2", "ipc2", "ipv2", "ipp2"); assertSubsItemDetails(state.results[1], "sp2", "ftp2", "ipc2", "ipv2", "ipp2");
} }
private static void assertItemDetails(ItemDetails item, String id, String title, String desc, private static void assertItemDetails(ItemDetails item, String id, String title, String desc,
...@@ -185,23 +180,113 @@ public class DigitalGoodsConverterTest { ...@@ -185,23 +180,113 @@ public class DigitalGoodsConverterTest {
assertEquals(responseCode, state.get()); assertEquals(responseCode, state.get());
} }
@Test
public void convertListPurchases() {
String id = "id";
String token = "token";
boolean acknowledged = true;
int state = 2;
long time = 1234L;
boolean autoRenew = true;
Bundle bundle = ListPurchasesConverter.createPurchaseDetailsBundle(
id, token, acknowledged, state, time, autoRenew);
PurchaseDetails details = ListPurchasesConverter.convertPurchaseDetails(bundle);
assertPurchaseDetails(details, id, token, acknowledged, state, time, autoRenew);
}
private static void assertPurchaseDetails(PurchaseDetails details, String itemId,
String purchaseToken, boolean acknowledged, int purchaseState, long purchaseTime,
boolean willAutoRenew) {
assertEquals(details.itemId, itemId);
assertEquals(details.purchaseToken, purchaseToken);
assertEquals(details.acknowledged, acknowledged);
assertEquals(details.purchaseState, purchaseState);
assertEquals(details.purchaseTime.microseconds, purchaseTime);
assertEquals(details.willAutoRenew, willAutoRenew);
}
@Test
public void convertListPurchases_wrongTypes() {
Bundle validBundle = ListPurchasesConverter.createPurchaseDetailsBundle(
"id", "token", true, 1, 2L, true);
assertNotNull(ListPurchasesConverter.convertPurchaseDetails(validBundle));
{
Bundle bundle = validBundle.deepCopy();
bundle.putInt(ListPurchasesConverter.KEY_ITEM_ID, 5);
assertNull(ListPurchasesConverter.convertPurchaseDetails(bundle));
}
{
Bundle bundle = validBundle.deepCopy();
bundle.putInt(ListPurchasesConverter.KEY_PURCHASE_TOKEN, 5);
assertNull(ListPurchasesConverter.convertPurchaseDetails(bundle));
}
{
Bundle bundle = validBundle.deepCopy();
bundle.putInt(ListPurchasesConverter.KEY_ACKNOWLEDGED, 5);
assertNull(ListPurchasesConverter.convertPurchaseDetails(bundle));
}
{
Bundle bundle = validBundle.deepCopy();
bundle.putBoolean(ListPurchasesConverter.KEY_PURCHASE_STATE, true);
assertNull(ListPurchasesConverter.convertPurchaseDetails(bundle));
}
{
Bundle bundle = validBundle.deepCopy();
bundle.putInt(ListPurchasesConverter.KEY_PURCHASE_TIME_MS_PAST_UNIX_EPOCH, 5);
assertNull(ListPurchasesConverter.convertPurchaseDetails(bundle));
}
{
Bundle bundle = validBundle.deepCopy();
bundle.putInt(ListPurchasesConverter.KEY_WILL_AUTO_RENEW, 5);
assertNull(ListPurchasesConverter.convertPurchaseDetails(bundle));
}
}
@Test
public void convertListPurchasesCallback() {
TestState<PurchaseDetails> state = new TestState<>();
ListPurchasesResponse callback = (responseCode, purchaseDetails) -> {
state.responseCode = responseCode;
state.results = purchaseDetails;
};
TrustedWebActivityCallback convertedCallback =
ListPurchasesConverter.convertCallback(callback);
int responseCode = 0;
Bundle args = ListPurchasesConverter.createResponseBundle(responseCode,
ListPurchasesConverter.createPurchaseDetailsBundle("1", "t1", true, 1, 1L, true),
ListPurchasesConverter.createPurchaseDetailsBundle("2", "t2", false, 2, 2L, false));
convertedCallback.onExtraCallback(ListPurchasesConverter.RESPONSE_COMMAND, args);
assertEquals(convertResponseCode(responseCode), state.responseCode);
assertPurchaseDetails(state.results[0], "1", "t1", true, 1, 1L, true);
assertPurchaseDetails(state.results[1], "2", "t2", false, 2, 2L, false);
}
@Test @Test
public void convertResponseCodes() { public void convertResponseCodes() {
assertEquals(BillingResponseCode.OK, assertEquals(BillingResponseCode.OK, convertResponseCode(PLAY_BILLING_OK));
DigitalGoodsConverter.convertResponseCodes(PLAY_BILLING_OK));
assertEquals(BillingResponseCode.ITEM_ALREADY_OWNED, assertEquals(BillingResponseCode.ITEM_ALREADY_OWNED,
DigitalGoodsConverter.convertResponseCodes(PLAY_BILLING_ITEM_ALREADY_OWNED)); convertResponseCode(PLAY_BILLING_ITEM_ALREADY_OWNED));
assertEquals(BillingResponseCode.ITEM_NOT_OWNED, assertEquals(BillingResponseCode.ITEM_NOT_OWNED,
DigitalGoodsConverter.convertResponseCodes(PLAY_BILLING_ITEM_NOT_OWNED)); convertResponseCode(PLAY_BILLING_ITEM_NOT_OWNED));
assertEquals(BillingResponseCode.ITEM_UNAVAILABLE, assertEquals(BillingResponseCode.ITEM_UNAVAILABLE,
DigitalGoodsConverter.convertResponseCodes(PLAY_BILLING_ITEM_UNAVAILABLE)); convertResponseCode(PLAY_BILLING_ITEM_UNAVAILABLE));
// Check that other numbers get set to ERROR. // Check that other numbers get set to ERROR.
assertEquals(BillingResponseCode.ERROR, assertEquals(BillingResponseCode.ERROR, convertResponseCode(2));
DigitalGoodsConverter.convertResponseCodes(2)); assertEquals(BillingResponseCode.ERROR, convertResponseCode(-1));
assertEquals(BillingResponseCode.ERROR, assertEquals(BillingResponseCode.ERROR, convertResponseCode(10));
DigitalGoodsConverter.convertResponseCodes(-1));
assertEquals(BillingResponseCode.ERROR,
DigitalGoodsConverter.convertResponseCodes(10));
} }
} }
\ No newline at end of file
...@@ -22,6 +22,12 @@ interface DigitalGoods { ...@@ -22,6 +22,12 @@ interface DigitalGoods {
// permanent upgrade). // permanent upgrade).
Acknowledge(string purchase_token, bool make_available_again) Acknowledge(string purchase_token, bool make_available_again)
=> (BillingResponseCode code); => (BillingResponseCode code);
// Queries the associated backend for information on all items that are
// currently owned by the user.
ListPurchases()
=> (BillingResponseCode code,
array<PurchaseDetails> purchase_details_list);
}; };
// Allow the renderer to request a |DigitalGoods| instance. DigitalGoods // Allow the renderer to request a |DigitalGoods| instance. DigitalGoods
...@@ -64,3 +70,19 @@ enum CreateDigitalGoodsResponseCode { ...@@ -64,3 +70,19 @@ enum CreateDigitalGoodsResponseCode {
kUnsupportedPaymentMethod, kUnsupportedPaymentMethod,
kUnsupportedContext, kUnsupportedContext,
}; };
struct PurchaseDetails {
string item_id;
string purchase_token;
bool acknowledged;
PurchaseState purchase_state;
// Microseconds since the Unix epoch.
mojo_base.mojom.TimeDelta purchase_time;
bool will_auto_renew;
};
enum PurchaseState {
kUnknown,
kPurchased,
kPending,
};
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