chromium/chrome/android/java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/sharing/TwaSharingController.java

// Copyright 2019 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.trustedwebactivityui.sharing;

import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Pair;

import androidx.annotation.Nullable;
import androidx.browser.trusted.sharing.ShareData;
import androidx.browser.trusted.sharing.ShareTarget;

import org.chromium.base.Promise;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.browserservices.intents.WebApkExtras;
import org.chromium.chrome.browser.browserservices.intents.WebApkShareTarget;
import org.chromium.chrome.browser.browserservices.metrics.TrustedWebActivityUmaRecorder;
import org.chromium.chrome.browser.browserservices.metrics.TrustedWebActivityUmaRecorder.ShareRequestMethod;
import org.chromium.chrome.browser.browserservices.ui.controller.Verifier;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityNavigationController;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityTabProvider;
import org.chromium.chrome.browser.dependency_injection.ActivityScope;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.webapps.WebApkPostShareTargetNavigator;
import org.chromium.content_public.browser.LoadUrlParams;

import java.util.ArrayList;
import java.util.Locale;
import java.util.function.Function;

import javax.inject.Inject;

/** Handles sharing intents coming to Trusted Web Activities. */
@ActivityScope
public class TwaSharingController {
    private final CustomTabActivityTabProvider mTabProvider;
    private final CustomTabActivityNavigationController mNavigationController;
    private final WebApkPostShareTargetNavigator mPostNavigator;
    private final Verifier mVerifierDelegate;
    private final TrustedWebActivityUmaRecorder mUmaRecorder;

    @Inject
    public TwaSharingController(
            CustomTabActivityTabProvider tabProvider,
            CustomTabActivityNavigationController navigationController,
            WebApkPostShareTargetNavigator postNavigator,
            Verifier verifierDelegate,
            TrustedWebActivityUmaRecorder umaRecorder) {
        mTabProvider = tabProvider;
        mNavigationController = navigationController;
        mPostNavigator = postNavigator;
        mVerifierDelegate = verifierDelegate;
        mUmaRecorder = umaRecorder;
    }

    /**
     * Checks whether the incoming intent (represented by a
     * {@link BrowserServicesIntentDataProvider}) is a sharing intent and attempts to perform the
     * sharing.
     *
     * Returns a {@link Promise<Boolean>} with a boolean telling whether sharing was successful.
     */
    public Promise<Boolean> deliverToShareTarget(
            BrowserServicesIntentDataProvider intentDataProvider) {
        ShareData shareData = intentDataProvider.getShareData();
        WebApkExtras webApkExtras = intentDataProvider.getWebApkExtras();
        WebApkShareTarget shareTarget =
                (webApkExtras != null)
                        ? webApkExtras.shareTarget
                        : toShareTargetInternal(intentDataProvider.getShareTarget());
        if (shareTarget == null || shareData == null) {
            return Promise.fulfilled(false);
        }
        Intent intent = intentDataProvider.getIntent();

        return mVerifierDelegate
                .verify(shareTarget.getAction())
                .then(
                        (Function<Boolean, Boolean>)
                                (verified) -> {
                                    if (!verified) {
                                        return false;
                                    }
                                    if (shareTarget.isShareMethodPost()) {
                                        boolean success = sendPost(shareData, shareTarget);
                                        if (success) {
                                            mUmaRecorder.recordShareTargetRequest(
                                                    ShareRequestMethod.POST);
                                        }
                                        return success;
                                    }

                                    mNavigationController.navigate(
                                            new LoadUrlParams(
                                                    computeStartUrlForGETShareTarget(
                                                            shareData, shareTarget)),
                                            intent);
                                    mUmaRecorder.recordShareTargetRequest(ShareRequestMethod.GET);
                                    return true;
                                });
    }

    /**
     * Converts to internal format.
     * TODO(pshmakov): pull WebApkShareTarget out of WebApkInfo and rename to
     * ShareTargetInternal. Also, replace WebApkInfo.ShareData with ShareData from TWA API.
     */
    private WebApkShareTarget toShareTargetInternal(@Nullable ShareTarget shareTarget) {
        if (shareTarget == null) return null;

        ShareTarget.Params params = shareTarget.params;
        String action = shareTarget.action;
        String paramTitle = params.title;
        String paramText = params.text;
        String method = shareTarget.method;
        boolean isPost = method != null && "POST".equals(method.toUpperCase(Locale.ENGLISH));
        String encodingType = shareTarget.encodingType;
        boolean isMultipart =
                encodingType != null
                        && "multipart/form-data".equals(encodingType.toLowerCase(Locale.ENGLISH));

        int numFiles = params.files == null ? 0 : params.files.size();
        String[] filesArray = new String[numFiles];
        String[][] acceptsArray = new String[numFiles][];
        for (int i = 0; i < numFiles; i++) {
            ShareTarget.FileFormField file = params.files.get(i);
            filesArray[i] = file.name;
            acceptsArray[i] = file.acceptedTypes.toArray(new String[file.acceptedTypes.size()]);
        }
        return new WebApkShareTarget(
                action, paramTitle, paramText, isPost, isMultipart, filesArray, acceptsArray);
    }

    private boolean sendPost(ShareData shareData, WebApkShareTarget target) {
        Tab tab = mTabProvider.getTab();
        if (tab == null) {
            assert false : "Null tab when sharing";
            return false;
        }
        return mPostNavigator.navigateIfPostShareTarget(
                target.getAction(), target, shareData, tab.getWebContents());
    }

    // Copy of HostBrowserLauncherParams#computeStartUrlForGETShareTarget().
    // Since the latter is in the WebAPK client code, we can't reuse it.
    private static String computeStartUrlForGETShareTarget(
            ShareData data, WebApkShareTarget target) {
        // These can be null, they are checked downstream.
        ArrayList<Pair<String, String>> entryList = new ArrayList<>();
        entryList.add(new Pair<>(target.getParamTitle(), data.title));
        entryList.add(new Pair<>(target.getParamText(), data.text));

        return createGETWebShareTargetUriString(target.getAction(), entryList);
    }

    private static String createGETWebShareTargetUriString(
            String action, ArrayList<Pair<String, String>> entryList) {
        Uri.Builder queryBuilder = new Uri.Builder();
        for (Pair<String, String> nameValue : entryList) {
            if (!TextUtils.isEmpty(nameValue.first) && !TextUtils.isEmpty(nameValue.second)) {
                // Uri.Builder does URL escaping.
                queryBuilder.appendQueryParameter(nameValue.first, nameValue.second);
            }
        }
        Uri shareUri = Uri.parse(action);
        Uri.Builder builder = shareUri.buildUpon();
        // Uri.Builder uses %20 rather than + for spaces, the spec requires +.
        String queryString = queryBuilder.build().toString();
        if (TextUtils.isEmpty(queryString)) {
            return action;
        }
        builder.encodedQuery(queryString.replace("%20", "+").substring(1));
        return builder.build().toString();
    }
}