chromium/chrome/android/java/src/org/chromium/chrome/browser/dragdrop/ChromeDragAndDropBrowserDelegate.java

// Copyright 2022 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.dragdrop;

import android.app.Activity;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.content.ClipData;
import android.content.ClipData.Item;
import android.content.ClipDescription;
import android.content.Intent;
import android.view.DragAndDropPermissions;
import android.view.DragEvent;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Log;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.content_public.browser.ContentFeatureMap;
import org.chromium.content_public.common.ContentFeatures;
import org.chromium.ui.base.MimeTypeUtils;
import org.chromium.ui.dragdrop.DragAndDropBrowserDelegate;
import org.chromium.ui.dragdrop.DragDropMetricUtils.UrlIntentSource;
import org.chromium.ui.dragdrop.DropDataAndroid;
import org.chromium.ui.dragdrop.DropDataProviderImpl;
import org.chromium.ui.dragdrop.DropDataProviderUtils;

/** Delegate for browser related functions used by Drag and Drop. */
public class ChromeDragAndDropBrowserDelegate implements DragAndDropBrowserDelegate {
    private static final String TAG = "ChromeDnDDelegate";
    private static final String PARAM_CLEAR_CACHE_DELAYED_MS = "ClearCacheDelayedMs";
    @VisibleForTesting static final String PARAM_DROP_IN_CHROME = "DropInChrome";

    private static Item sItemWithPendingIntentForTesting;
    private static boolean sDefinedItemWithPendingIntentForTesting;
    private static boolean sClipDataItemBuilderNotFound;

    private final String[] mSupportedMimeTypes =
            new String[] {
                MimeTypeUtils.CHROME_MIMETYPE_TAB,
                ClipDescription.MIMETYPE_TEXT_PLAIN,
                ClipDescription.MIMETYPE_TEXT_INTENT,
                MimeTypeUtils.CHROME_MIMETYPE_LINK
            };

    private final Supplier<Activity> mActivitySupplier;
    private final boolean mSupportDropInChrome;
    private final boolean mSupportAnimatedImageDragShadow;

    /**
     * @param activitySupplier The supplier to get the Activity this delegate is associated with.
     */
    public ChromeDragAndDropBrowserDelegate(Supplier<Activity> activitySupplier) {
        mActivitySupplier = activitySupplier;
        mSupportDropInChrome =
                ContentFeatureMap.getInstance()
                        .getFieldTrialParamByFeatureAsBoolean(
                                ContentFeatures.TOUCH_DRAG_AND_CONTEXT_MENU,
                                PARAM_DROP_IN_CHROME,
                                false);
        mSupportAnimatedImageDragShadow =
                ChromeFeatureList.isEnabled(ChromeFeatureList.ANIMATED_IMAGE_DRAG_SHADOW);

        int delay =
                ContentFeatureMap.getInstance()
                        .getFieldTrialParamByFeatureAsInt(
                                ContentFeatures.TOUCH_DRAG_AND_CONTEXT_MENU,
                                PARAM_CLEAR_CACHE_DELAYED_MS,
                                DropDataProviderImpl.DEFAULT_CLEAR_CACHED_DATA_INTERVAL_MS);
        DropDataProviderUtils.setClearCachedDataIntervalMs(delay);
    }

    @Override
    public boolean getSupportDropInChrome() {
        return mSupportDropInChrome;
    }

    @Override
    public boolean getSupportAnimatedImageDragShadow() {
        return mSupportAnimatedImageDragShadow;
    }

    @Override
    public DragAndDropPermissions getDragAndDropPermissions(DragEvent dropEvent) {
        assert mSupportDropInChrome : "Should only be accessed when drop in Chrome.";

        if (mActivitySupplier.get() == null) {
            return null;
        }
        return mActivitySupplier.get().requestDragAndDropPermissions(dropEvent);
    }

    @Override
    public Intent createUrlIntent(String urlString, @UrlIntentSource int intentSrc) {
        Intent intent = null;
        Activity activity = mActivitySupplier.get();
        if (activity != null && MultiWindowUtils.isMultiInstanceApi31Enabled()) {
            intent =
                    DragAndDropLauncherActivity.getLinkLauncherIntent(
                            activity,
                            urlString,
                            MultiWindowUtils.getInstanceIdForLinkIntent(activity),
                            intentSrc);
        }
        return intent;
    }

    @Override
    public ClipData buildClipData(@NonNull DropDataAndroid dropData) {
        assert dropData instanceof ChromeDropDataAndroid;
        ChromeDropDataAndroid chromeDropDataAndroid = (ChromeDropDataAndroid) dropData;
        if (chromeDropDataAndroid.hasTab() && chromeDropDataAndroid.allowTabDragToCreateInstance) {
            ClipData clipData = buildClipDataForTabTearing(chromeDropDataAndroid.tab);
            if (clipData != null) return clipData;
        }
        String text =
                chromeDropDataAndroid.hasTab()
                        ? chromeDropDataAndroid.buildTabClipDataText()
                        : dropData.text;
        return new ClipData(null, mSupportedMimeTypes, new Item(text));
    }

    private @Nullable ClipData buildClipDataForTabTearing(Tab tab) {
        Intent intent =
                DragAndDropLauncherActivity.getTabIntent(
                        tab.getContext(), tab, MultiWindowUtils.INVALID_INSTANCE_ID);
        if (intent != null) {
            ActivityOptions opts = ActivityOptions.makeBasic();
            ApiCompatibilityUtils.setCreatorActivityOptionsBackgroundActivityStartMode(opts);
            PendingIntent pendingIntent =
                    PendingIntent.getActivity(
                            tab.getContext(),
                            0,
                            intent,
                            PendingIntent.FLAG_IMMUTABLE,
                            opts.toBundle());
            Item item = buildClipDataItemWithPendingIntent(pendingIntent);
            return item == null
                    ? new ClipData(null, mSupportedMimeTypes, new Item(intent))
                    : new ClipData(null, mSupportedMimeTypes, item);
        }
        return null;
    }

    @Override
    public int buildFlags(int originalFlag, DropDataAndroid dropData) {
        assert dropData instanceof ChromeDropDataAndroid;
        ChromeDropDataAndroid chromeDropData = (ChromeDropDataAndroid) dropData;
        if (!chromeDropData.hasTab() || !chromeDropData.allowTabDragToCreateInstance) {
            return originalFlag;
        }
        return originalFlag
                | View.DRAG_FLAG_GLOBAL_SAME_APPLICATION
                | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG;
    }

    @SuppressWarnings("NewApi")
    private static ClipData.Item buildClipDataItemWithPendingIntent(PendingIntent pendingIntent) {
        if (sDefinedItemWithPendingIntentForTesting) return sItemWithPendingIntentForTesting;
        // This invocation is wrapped in a try-catch block to allow backporting of the
        // ClipData.Item.Builder() class on pre-V devices. On pre-V devices not supporting this,
        // state will be cached on the first failure to avoid subsequent invalid attempts.
        if (sClipDataItemBuilderNotFound) return null;
        try {
            return new ClipData.Item.Builder()
                    .setIntentSender(pendingIntent.getIntentSender())
                    .build();
        } catch (NoClassDefFoundError e) {
            Log.w(TAG, e.toString());
            sClipDataItemBuilderNotFound = true;
        }
        return null;
    }

    /** Sets the ClipData.Item with a PendingIntent for testing purposes. */
    public static void setClipDataItemWithPendingIntentForTesting(Item item) {
        sItemWithPendingIntentForTesting = item;
        sDefinedItemWithPendingIntentForTesting = true;
        ResettersForTesting.register(
                () -> {
                    sDefinedItemWithPendingIntentForTesting = false;
                    sItemWithPendingIntentForTesting = null;
                });
    }
}