chromium/chrome/android/java/src/org/chromium/chrome/browser/dragdrop/toolbar/ToolbarDragDropCoordinator.java

// Copyright 2023 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.toolbar;

import android.os.SystemClock;
import android.view.DragEvent;
import android.view.View;
import android.view.View.OnDragListener;
import android.widget.FrameLayout;

import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.omnibox.OmniboxFocusReason;
import org.chromium.chrome.browser.omnibox.OmniboxStub;
import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteDelegate;
import org.chromium.chrome.browser.omnibox.suggestions.OmniboxLoadUrlParams;
import org.chromium.components.search_engines.TemplateUrlService;
import org.chromium.ui.base.MimeTypeUtils;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.dragdrop.DropDataAndroid;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import org.chromium.ui.widget.Toast;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/** ToolbarDragDrop Coordinator owns the view and the model change processor. */
public class ToolbarDragDropCoordinator implements OnDragListener {
    /**
     * Java Enum of AndroidToolbarDropType used for histogram recording for
     * Android.DragDrop.ToOmnibox.DropType. This is used for histograms and should therefore
     * be treated as append-only.
     */
    @IntDef({
        DropType.INVALID,
        DropType.TEXT,
        DropType.CHROME_TEXT,
        DropType.CHROME_LINK,
        DropType.CHROME_IMAGE,
        DropType.NUM_ENTRIES
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface DropType {
        int INVALID = 0;
        int CHROME_TEXT = 1;
        int TEXT = 2;
        int CHROME_LINK = 3;
        int CHROME_IMAGE = 4;

        int NUM_ENTRIES = 5;
    }

    // TODO(crbug.com/40277406): Swap error message to a translated string.
    private static final String ERROR_MESSAGE = "Unable to handle drop";
    private AutocompleteDelegate mAutocompleteDelegate;
    private PropertyModelChangeProcessor mModelChangeProcessor;
    private FrameLayout mTargetView;
    private TargetViewDragListener mTargetViewDragListener;
    private PropertyModel mModel;
    private OmniboxStub mOmniboxStub;
    private Supplier<TemplateUrlService> mTemplateUrlServiceSupplier;

    interface OnDropCallback {
        /** Handles parsing DragEvents on a drop to the Omnibox */
        void parseDragEvent(DragEvent event);
    }

    /**
     * Create the Coordinator of TargetView that owns the view and the change process.
     *
     * @param targetView is the view displayed during the drag and drop process.
     * @param autocompleteDelegate Used to navigate on a successful link drop.
     * @param omniboxStub Used to navigate a successful text drop.
     * @param templateUrlServiceSupplier Used to obtain the TemplateUrlService needed on an image
     * drop.
     *
     */
    public ToolbarDragDropCoordinator(
            FrameLayout targetView,
            AutocompleteDelegate autocompleteDelegate,
            OmniboxStub omniboxStub,
            Supplier<TemplateUrlService> templateUrlServiceSupplier) {
        // TargetView is inflated in order to have the onDragListener attached before it is visible
        mTargetView = targetView;
        mModel =
                new PropertyModel.Builder(TargetViewProperties.ALL_KEYS)
                        .with(TargetViewProperties.TARGET_VIEW_VISIBLE, View.GONE)
                        .build();
        mAutocompleteDelegate = autocompleteDelegate;
        mTargetViewDragListener = new TargetViewDragListener(mModel, this::parseDragEvent);
        mModel.set(TargetViewProperties.ON_DRAG_LISTENER, mTargetViewDragListener);
        mModelChangeProcessor =
                PropertyModelChangeProcessor.create(mModel, mTargetView, new TargetViewBinder());
        mOmniboxStub = omniboxStub;
        mTemplateUrlServiceSupplier = templateUrlServiceSupplier;
    }

    @Override
    public boolean onDrag(View v, DragEvent event) {
        switch (event.getAction()) {
            case DragEvent.ACTION_DRAG_STARTED:
                if (isValidMimeType(event)) {
                    mModel.set(TargetViewProperties.TARGET_VIEW_VISIBLE, View.VISIBLE);
                    return true;
                }
                return false;
            case DragEvent.ACTION_DRAG_ENDED:
                mModel.set(TargetViewProperties.TARGET_VIEW_VISIBLE, View.GONE);
                // fall through
            default:
                return false;
        }
    }

    /** Destroy the Toolbar Drag and Drop Coordinator and its components. */
    public void destroy() {
        mModel.set(TargetViewProperties.ON_DRAG_LISTENER, null);
        mTargetView.setVisibility(View.GONE);
        mModelChangeProcessor.destroy();
    }

    // Checks if the dragged object is supported for dragging into Omnibox
    public static boolean isValidMimeType(DragEvent event) {
        return event.getClipDescription().filterMimeTypes(MimeTypeUtils.TEXT_MIME_TYPE) != null;
    }

    /** Handles parsing DragEvents on a drop to the Omnibox. */
    @VisibleForTesting
    void parseDragEvent(DragEvent event) {
        if (event.getClipDescription().filterMimeTypes(MimeTypeUtils.IMAGE_MIME_TYPE) != null) {
            // Try to get the byte array needed for image search from local state. If drag and drop
            // is started from the same Chrome activity, then DropDataAndroid should be set as a
            // local state.
            //  TODO(crbug.com/40277338): Read the image bytes from localState using a util method
            Object dropData = event.getLocalState();
            TemplateUrlService urlService = mTemplateUrlServiceSupplier.get();
            if (!urlService.isSearchByImageAvailable()
                    || (!(dropData instanceof DropDataAndroid))) {
                handleErrorToast();
                return;
            }
            String[] postData = urlService.getImageUrlAndPostContent();
            // TODO(crbug.com/40278861): Pass in correct imageByteArray to AutocompleteDelegate
            byte[] imageByteArray = new byte[0];
            mAutocompleteDelegate.loadUrl(
                    new OmniboxLoadUrlParams.Builder(postData[0], PageTransition.GENERATED)
                            .setInputStartTimestamp(SystemClock.uptimeMillis())
                            .setpostDataAndType(imageByteArray, postData[1])
                            .build());
            recordDropType(DropType.CHROME_IMAGE);
        } else if (event.getClipDescription().hasMimeType(MimeTypeUtils.CHROME_MIMETYPE_TEXT)) {
            mOmniboxStub.setUrlBarFocus(
                    true,
                    event.getClipData()
                            .getItemAt(0)
                            .coerceToText(mTargetView.getContext())
                            .toString(),
                    OmniboxFocusReason.DRAG_DROP_TO_OMNIBOX);
            recordDropType(DropType.CHROME_TEXT);
        } else if (event.getClipDescription().hasMimeType(MimeTypeUtils.CHROME_MIMETYPE_LINK)) {
            /**
             * This parsing is based on the implementation in DragAndDropDelegateImpl#BuildClipData.
             * Ideally we should handle build / parsing in a similar place to keep things
             * consistent. TODO(crbug.com/40277338): Build ClipData and parse link URL using a
             * static helper method
             */
            String url = event.getClipData().getItemAt(0).getIntent().getData().toString();
            mAutocompleteDelegate.loadUrl(
                    new OmniboxLoadUrlParams.Builder(url, PageTransition.TYPED)
                            .setInputStartTimestamp(SystemClock.uptimeMillis())
                            .setOpenInNewTab(false)
                            .build());
            recordDropType(DropType.CHROME_LINK);
        } else {
            // case where dragged object is not from Chrome
            event.getClipDescription().filterMimeTypes(MimeTypeUtils.TEXT_MIME_TYPE);
            if (event.getClipData() == null) {
                handleErrorToast();
                recordDropType(DropType.INVALID);
            } else {
                mOmniboxStub.setUrlBarFocus(
                        true,
                        event.getClipData()
                                .getItemAt(0)
                                .coerceToText(mTargetView.getContext())
                                .toString(),
                        OmniboxFocusReason.DRAG_DROP_TO_OMNIBOX);
                recordDropType(DropType.TEXT);
            }
        }
    }

    private void handleErrorToast() {
        Toast errorMessage =
                Toast.makeText(mTargetView.getContext(), ERROR_MESSAGE, Toast.LENGTH_SHORT);
        errorMessage.show();
    }

    private void recordDropType(@DropType int type) {
        RecordHistogram.recordEnumeratedHistogram(
                "Android.DragDrop.ToOmnibox.DropType", type, DropType.NUM_ENTRIES);
    }
}