chromium/chrome/android/java/src/org/chromium/chrome/browser/browserservices/digitalgoods/GetDetailsConverter.java

// Copyright 2020 The Chromium Authors
// 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.payments.mojom.BillingResponseCode;
import org.chromium.payments.mojom.DigitalGoods.GetDetails_Response;
import org.chromium.payments.mojom.ItemDetails;
import org.chromium.payments.mojom.ItemType;
import org.chromium.payments.mojom.PaymentCurrencyAmount;
import org.chromium.url.mojom.Url;

/** A converter that deals with the parameters and result for GetDetails calls. */
public class GetDetailsConverter {
    private static final String TAG = "DigitalGoods";

    static final String PARAM_GET_DETAILS_ITEM_IDS = "getDetails.itemIds";
    public static final String RESPONSE_COMMAND = "getDetails.response";
    static final String KEY_RESPONSE_CODE = "getDetails.responseCode";
    static final String KEY_DETAILS_LIST = "getDetails.detailsList";

    static final String KEY_ID = "itemDetails.id";
    static final String KEY_TITLE = "itemDetails.title";
    static final String KEY_DESC = "itemDetails.description";
    static final String KEY_CURRENCY = "itemDetails.currency";
    static final String KEY_VALUE = "itemDetails.value";
    static final String[] REQUIRED_FIELDS = {KEY_ID, KEY_TITLE, KEY_DESC, KEY_CURRENCY, KEY_VALUE};

    static final String KEY_TYPE = "itemDetails.type";
    static final String KEY_ICON_URL = "itemDetails.url";

    static final String KEY_SUBS_PERIOD = "itemDetails.subsPeriod";
    static final String KEY_FREE_TRIAL_PERIOD = "itemDetails.freeTrialPeriod";
    static final String KEY_INTRO_CURRENCY = "itemDetails.introPriceCurrency";
    static final String KEY_INTRO_VALUE = "itemDetails.introPriceValue";
    static final String KEY_INTRO_PERIOD = "itemDetails.introPricePeriod";
    static final String KEY_INTRO_CYCLES = "itemDetails.introPriceCycles";

    static final String ITEM_TYPE_SUBS = "subs";
    static final String ITEM_TYPE_INAPP = "inapp";

    private GetDetailsConverter() {}

    /** Converts the parameters to the getDetails. */
    static Bundle convertParams(String[] itemIds) {
        Bundle args = new Bundle();
        args.putStringArray(GetDetailsConverter.PARAM_GET_DETAILS_ITEM_IDS, itemIds);
        return args;
    }

    /**
     * Produces a {@link TrustedWebActivityCallback} that calls the given
     * {@link GetDetailsResponse}.
     */
    static TrustedWebActivityCallback convertCallback(GetDetails_Response 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_DETAILS_LIST, Parcelable[].class)) {
                    returnClientAppError(callback);
                    return;
                }

                int code = args.getInt(KEY_RESPONSE_CODE);
                Parcelable[] array = args.getParcelableArray(KEY_DETAILS_LIST);

                ItemDetails[] details =
                        convertParcelableArray(array, GetDetailsConverter::convertItemDetails)
                                .toArray(new ItemDetails[0]);
                callback.call(convertResponseCode(code, args), details);
            }
        };
    }

    /** Extracts an {@link ItemDetails} from a {@link Parcelable}. */
    static @Nullable ItemDetails convertItemDetails(Bundle item) {
        for (String field : REQUIRED_FIELDS) {
            if (item.containsKey(field) && (item.get(field) instanceof String)) continue;
            Log.w(TAG, "Item does not contain field String " + field + ".");
            return null;
        }

        // Mandatory fields.
        PaymentCurrencyAmount price = new PaymentCurrencyAmount();
        price.currency = item.getString(KEY_CURRENCY);
        price.value = item.getString(KEY_VALUE);

        ItemDetails result = new ItemDetails();
        result.itemId = item.getString(KEY_ID);
        result.title = item.getString(KEY_TITLE);
        result.description = item.getString(KEY_DESC);
        result.price = price;

        // Optional fields.
        result.type = convertItemType(item.getString(KEY_TYPE));
        String iconUrl = item.getString(KEY_ICON_URL);
        if (iconUrl != null) {
            org.chromium.url.mojom.Url url = new Url();
            url.url = iconUrl;
            result.iconUrls = new Url[] {url};
        } else {
            result.iconUrls = new Url[0];
        }

        result.subscriptionPeriod = item.getString(KEY_SUBS_PERIOD);
        result.freeTrialPeriod = item.getString(KEY_FREE_TRIAL_PERIOD);
        result.introductoryPricePeriod = item.getString(KEY_INTRO_PERIOD);
        result.introductoryPriceCycles = item.getInt(KEY_INTRO_CYCLES, 0);

        String introPriceCurrency = item.getString(KEY_INTRO_CURRENCY);
        String introPriceValue = item.getString(KEY_INTRO_VALUE);

        if (introPriceCurrency != null && introPriceValue != null) {
            PaymentCurrencyAmount introPrice = new PaymentCurrencyAmount();
            introPrice.currency = introPriceCurrency;
            introPrice.value = introPriceValue;
            result.introductoryPrice = introPrice;
        }

        return result;
    }

    static int convertItemType(@Nullable String itemType) {
        // TODO: introduce our own API here.
        if (ITEM_TYPE_SUBS.equals(itemType)) {
            return ItemType.SUBSCRIPTION;
        } else if (ITEM_TYPE_INAPP.equals(itemType)) {
            return ItemType.PRODUCT;
        }

        return ItemType.UNKNOWN;
    }

    static void returnClientAppUnavailable(GetDetails_Response callback) {
        callback.call(BillingResponseCode.CLIENT_APP_UNAVAILABLE, new ItemDetails[0]);
    }

    static void returnClientAppError(GetDetails_Response callback) {
        callback.call(BillingResponseCode.CLIENT_APP_ERROR, new ItemDetails[0]);
    }

    /**
     * Creates a {@link Bundle} that represents an {@link ItemDetails} with the given values.
     * This would be used by the client app and is here only to help testing.
     */
    @VisibleForTesting
    static Bundle createItemDetailsBundle(
            String id,
            String title,
            String desc,
            String currency,
            String value,
            String type,
            String iconUrl,
            @Nullable String subsPeriod,
            @Nullable String freeTrialPeriod,
            @Nullable String introPriceCurrency,
            @Nullable String introPriceValue,
            @Nullable String intoPricePeriod,
            int introPriceCycles) {
        Bundle bundle = createItemDetailsBundle(id, title, desc, currency, value);

        bundle.putString(KEY_TYPE, type);
        bundle.putString(KEY_ICON_URL, iconUrl);

        bundle.putString(KEY_SUBS_PERIOD, subsPeriod);
        bundle.putString(KEY_FREE_TRIAL_PERIOD, freeTrialPeriod);
        bundle.putString(KEY_INTRO_CURRENCY, introPriceCurrency);
        bundle.putString(KEY_INTRO_VALUE, introPriceValue);
        bundle.putString(KEY_INTRO_PERIOD, intoPricePeriod);
        bundle.putInt(KEY_INTRO_CYCLES, introPriceCycles);

        return bundle;
    }

    /** Like the above method, but provides {@code null} for all optional parameters. */
    @VisibleForTesting
    static Bundle createItemDetailsBundle(
            String id, String title, String desc, String currency, String value) {
        Bundle bundle = new Bundle();

        bundle.putString(KEY_ID, id);
        bundle.putString(KEY_TITLE, title);
        bundle.putString(KEY_DESC, desc);
        bundle.putString(KEY_CURRENCY, currency);
        bundle.putString(KEY_VALUE, value);

        return bundle;
    }

    /**
     * Creates a {@link Bundle} that represents the result of a getDetails call. This would be
     * carried out by the client app and is only here to help testing.
     */
    @VisibleForTesting
    static Bundle createResponseBundle(int responseCode, Bundle... itemDetails) {
        Bundle bundle = new Bundle();

        bundle.putInt(KEY_RESPONSE_CODE, responseCode);
        bundle.putParcelableArray(KEY_DETAILS_LIST, itemDetails);

        return bundle;
    }
}