chromium/chrome/android/java/src/org/chromium/chrome/browser/payments/ui/PaymentAppComparator.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.payments.ui;

import org.chromium.chrome.browser.payments.PaymentPreferencesUtil;
import org.chromium.components.autofill.Completable;
import org.chromium.components.payments.PaymentApp;
import org.chromium.components.payments.PaymentRequestParams;
import org.chromium.payments.mojom.PaymentOptions;

import java.util.Comparator;

/**
 * A comparator that is used to rank the payment apps to be listed on the PaymentRequest
 * UI.
 */
/* package */ class PaymentAppComparator implements Comparator<PaymentApp> {
    private final PaymentRequestParams mParams;

    /**
     * Create an instance of PaymentAppComparator.
     * @param params The parameters of PaymentRequest specified by the merchant.
     */
    /* package */ PaymentAppComparator(PaymentRequestParams params) {
        mParams = params;
    }

    /**
     * Compares two payment apps by ranking score.
     * Return negative value if a has strictly lower ranking score than b.
     * Return zero if a and b have the same ranking score.
     * Return positive value if a has strictly higher ranking score than b.
     */
    private static int compareAppsByRankingScore(PaymentApp a, PaymentApp b) {
        int aCount = PaymentPreferencesUtil.getPaymentAppUseCount(a.getIdentifier());
        int bCount = PaymentPreferencesUtil.getPaymentAppUseCount(b.getIdentifier());
        long aDate = PaymentPreferencesUtil.getPaymentAppLastUseDate(a.getIdentifier());
        long bDate = PaymentPreferencesUtil.getPaymentAppLastUseDate(a.getIdentifier());

        return Double.compare(getRankingScore(aCount, aDate), getRankingScore(bCount, bDate));
    }

    /**
     * Compares two Completable by completeness score.
     * Return negative value if a has strictly lower completeness score than b.
     * Return zero if a and b have the same completeness score.
     * Return positive value if a has strictly higher completeness score than b.
     */
    /* package */ static int compareCompletablesByCompleteness(Completable a, Completable b) {
        return Integer.compare(a.getCompletenessScore(), b.getCompletenessScore());
    }

    /**
     * The ranking score is calculated according to use count and last use date. The formula is
     * the same as the one used in GetRankingScore in autofill_data_model.cc.
     */
    private static double getRankingScore(int count, long date) {
        long currentTime = System.currentTimeMillis();
        return -Math.log((currentTime - date) / (24 * 60 * 60 * 1000) + 2) / Math.log(count + 2);
    }

    /**
     * Sorts the payment apps by several rules:
     * Rule 1: Complete apps before incomplete apps.
     * Rule 2: When shipping address is requested, apps which will handle shipping address before
     * others.
     * Rule 3: When payer's contact information is requested, apps which will handle more required
     * contact fields (name, email, phone) come before others.
     * Rule 4: Preselectable apps before non-preselectable apps.
     * Rule 5: Frequently and recently used apps before rarely and non-recently used apps.
     */
    @Override
    public int compare(PaymentApp a, PaymentApp b) {
        // Complete cards before cards with missing information.
        int completeness = compareCompletablesByCompleteness(b, a);
        if (completeness != 0) return completeness;

        PaymentOptions options = mParams.getPaymentOptions();
        if (options != null) {
            // Payment apps which handle shipping address before others.
            if (options.requestShipping) {
                int canHandleShipping =
                        (b.handlesShippingAddress() ? 1 : 0) - (a.handlesShippingAddress() ? 1 : 0);
                if (canHandleShipping != 0) return canHandleShipping;
            }

            // Payment apps which handle more contact information fields come first.
            int aSupportedContactDelegationsNum = 0;
            int bSupportedContactDelegationsNum = 0;
            if (options.requestPayerName) {
                if (a.handlesPayerName()) aSupportedContactDelegationsNum++;
                if (b.handlesPayerName()) bSupportedContactDelegationsNum++;
            }
            if (options.requestPayerEmail) {
                if (a.handlesPayerEmail()) aSupportedContactDelegationsNum++;
                if (b.handlesPayerEmail()) bSupportedContactDelegationsNum++;
            }
            if (options.requestPayerPhone) {
                if (a.handlesPayerPhone()) aSupportedContactDelegationsNum++;
                if (b.handlesPayerPhone()) bSupportedContactDelegationsNum++;
            }
            if (bSupportedContactDelegationsNum != aSupportedContactDelegationsNum) {
                return bSupportedContactDelegationsNum - aSupportedContactDelegationsNum > 0
                        ? 1
                        : -1;
            }
        }

        // Preselectable apps before non-preselectable apps.
        // Note that this only affects service worker payment apps' apps for now
        // since autofill cards have already been sorted by preselect after sorting by completeness.
        // And the other payment apps can always be preselected.
        int canPreselect = (b.canPreselect() ? 1 : 0) - (a.canPreselect() ? 1 : 0);
        if (canPreselect != 0) return canPreselect;

        // More frequently and recently used apps first.
        return compareAppsByRankingScore(b, a);
    }
}