chromium/ui/android/junit/src/org/chromium/ui/dragdrop/DragAndDropDelegateImplUnitTest.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.ui.dragdrop;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ProviderInfo;
import android.graphics.Bitmap;
import android.graphics.drawable.VectorDrawable;
import android.view.DragAndDropPermissions;
import android.view.DragEvent;
import android.view.View;
import android.view.View.DragShadowBuilder;
import android.view.View.MeasureSpec;
import android.widget.ImageView;

import androidx.annotation.Nullable;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowContentResolver;

import org.chromium.base.ContextUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.ui.accessibility.AccessibilityState;
import org.chromium.ui.base.UiAndroidFeatureList;
import org.chromium.ui.dragdrop.DragAndDropDelegateImpl.DragTargetType;
import org.chromium.url.JUnitTestGURLs;

/** Unit tests for {@link DragAndDropDelegateImpl}. */
@RunWith(BaseRobolectricTestRunner.class)
public class DragAndDropDelegateImplUnitTest {
    /** Using a window size of 1000*600 for the ease of dp / pixel calculation. */
    private static final int WINDOW_WIDTH = 1000;

    private static final int WINDOW_HEIGHT = 600;
    private static final float DRAG_START_X_DP = 1.0f;
    private static final float DRAG_START_Y_DP = 1.0f;
    private static final String IMAGE_FILENAME = "image.png";

    @Mock private DragAndDropPermissions mDragAndDropPermissions;
    @Mock private DragAndDropBrowserDelegate mDragAndDropBrowserDelegate;
    @Spy private View mContainerView;

    private DragAndDropDelegateImpl mDragAndDropDelegateImpl;
    private DropDataProviderImpl mDropDataProviderImpl;
    private @Nullable DragShadowBuilder mLastDragShadowBuilder;

    @Before
    public void setup() {
        MockitoAnnotations.openMocks(this);

        Context context = ContextUtils.getApplicationContext();
        mDropDataProviderImpl = new DropDataProviderImpl();
        mDragAndDropDelegateImpl = new DragAndDropDelegateImpl();
        DropDataContentProvider provider = new DropDataContentProvider();
        ProviderInfo providerInfo = new ProviderInfo();
        providerInfo.authority = DropDataProviderImpl.FULL_AUTH_URI.getAuthority();
        provider.attachInfo(context, providerInfo);
        provider.setDropDataProviderImpl(mDropDataProviderImpl);

        ShadowContentResolver.registerProviderInternal(
                DropDataProviderImpl.FULL_AUTH_URI.getAuthority(), provider);
        mContainerView = Mockito.spy(new View(context));
        doAnswer(
                        invocationOnMock -> {
                            mLastDragShadowBuilder = invocationOnMock.getArgument(1);
                            return true;
                        })
                .when(mContainerView)
                .startDragAndDrop(
                        any(ClipData.class), any(DragShadowBuilder.class), any(), anyInt());
        View rootView = mContainerView.getRootView();
        rootView.measure(
                MeasureSpec.makeMeasureSpec(WINDOW_WIDTH, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(WINDOW_HEIGHT, MeasureSpec.EXACTLY));
        rootView.layout(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
    }

    @After
    public void tearDown() {
        mDropDataProviderImpl.onDragEnd(false);
        AccessibilityState.setIsAnyAccessibilityServiceEnabledForTesting(false);
    }

    @Test
    public void testStartDragAndDrop_Text() {
        final Bitmap shadowImage = Bitmap.createBitmap(100, 200, Bitmap.Config.ALPHA_8);
        final DropDataAndroid dropData = DropDataAndroid.create("text", null, null, null, null);

        mDragAndDropDelegateImpl.startDragAndDrop(
                mContainerView,
                shadowImage,
                dropData,
                mContainerView.getContext(),
                /* cursorOffsetX= */ 0,
                /* cursorOffsetY= */ 0,
                /* dragObjRectWidth= */ 100,
                /* dragObjRectHeight= */ 200);

        Assert.assertTrue("Drag should be started.", mDragAndDropDelegateImpl.isDragStarted());
        Assert.assertEquals(
                "Drag shadow width not match. Should not resize for text.",
                100,
                mDragAndDropDelegateImpl.getDragShadowWidth());
        Assert.assertEquals(
                "Drag shadow height not match. Should not resize for text.",
                200,
                mDragAndDropDelegateImpl.getDragShadowHeight());
        assertDragTypeNotRecorded("Drag didn't end.");

        mDragAndDropDelegateImpl.onDrag(mContainerView, mockDragEvent(DragEvent.ACTION_DRAG_ENDED));

        Assert.assertFalse("Drag should end.", mDragAndDropDelegateImpl.isDragStarted());
        Assert.assertEquals(
                "Drag shadow width should be reset.",
                0,
                mDragAndDropDelegateImpl.getDragShadowWidth());
        Assert.assertEquals(
                "Drag shadow height should be reset.",
                0,
                mDragAndDropDelegateImpl.getDragShadowHeight());
        assertDragTypeRecorded(DragTargetType.TEXT);
        assertDragOutsideWebContentHistogramsRecorded(/* dropResult= */ false);
    }

    @Test
    @Config(shadows = ShadowContentResolver.class)
    public void testStartDragAndDrop_Image() {
        final Bitmap shadowImage = Bitmap.createBitmap(100, 200, Bitmap.Config.ALPHA_8);
        final DropDataAndroid imageDropData =
                DropDataAndroid.create("", null, new byte[] {1, 2, 3, 4}, "png", IMAGE_FILENAME);
        mDragAndDropDelegateImpl.startDragAndDrop(
                mContainerView,
                shadowImage,
                imageDropData,
                mContainerView.getContext(),
                /* cursorOffsetX= */ 0,
                /* cursorOffsetY= */ 0,
                /* dragObjRectWidth= */ 100,
                /* dragObjRectHeight= */ 200);
        Assert.assertFalse(
                "The DragShadowBuilder should not be AnimatedImageDragShadowBuilder.",
                mLastDragShadowBuilder instanceof AnimatedImageDragShadowBuilder);
        // width = scaledShadowSize + padding * 2 = 100 * 0.6 + 1 * 2 = 62
        Assert.assertEquals(
                "Drag shadow width not match. Should do resize for image and add 1dp border.",
                62,
                mDragAndDropDelegateImpl.getDragShadowWidth());
        // height = scaledShadowSize + padding * 2 = 200 * 0.6 + 1 * 2 = 122
        Assert.assertEquals(
                "Drag shadow height not match. Should do resize for image and add 1dp border.",
                122,
                mDragAndDropDelegateImpl.getDragShadowHeight());
        Assert.assertNotNull(
                "Cached Image bytes should not be null.",
                mDropDataProviderImpl.getImageBytesForTesting());
        assertDragTypeNotRecorded("Drag didn't end.");

        DragEvent dragEnd = mockDragEvent(DragEvent.ACTION_DRAG_ENDED);
        mDragAndDropDelegateImpl.onDrag(mContainerView, dragEnd);
        Assert.assertNull(
                "Cached Image bytes should be cleaned.",
                mDropDataProviderImpl.getImageBytesForTesting());
        assertDragTypeRecorded(DragTargetType.IMAGE);
        assertDragOutsideWebContentHistogramsRecorded(/* dropResult= */ false);
    }

    @Test
    @Config(shadows = ShadowContentResolver.class)
    public void testStartDragAndDrop_Image_WithAnimation() {
        mDragAndDropDelegateImpl.setDragAndDropBrowserDelegate(
                mockDragAndDropBrowserDelegate(false, true, null, null));
        final Bitmap shadowImage = Bitmap.createBitmap(100, 200, Bitmap.Config.ALPHA_8);
        final DropDataAndroid imageDropData =
                DropDataAndroid.create("", null, new byte[] {1, 2, 3, 4}, "png", IMAGE_FILENAME);
        mDragAndDropDelegateImpl.startDragAndDrop(
                mContainerView,
                shadowImage,
                imageDropData,
                mContainerView.getContext(),
                /* cursorOffsetX= */ 0,
                /* cursorOffsetY= */ 0,
                /* dragObjRectWidth= */ 100,
                /* dragObjRectHeight= */ 200);
        Assert.assertTrue(
                "The DragShadowBuilder should be AnimatedImageDragShadowBuilder.",
                mLastDragShadowBuilder instanceof AnimatedImageDragShadowBuilder);
        // width = scaledShadowSize + padding * 2 = 100 * 0.6 + 1 * 2 = 62
        Assert.assertEquals(
                "Drag shadow width not match. Should do resize for image and add 1dp border.",
                62,
                mDragAndDropDelegateImpl.getDragShadowWidth());
        // height = scaledShadowSize + padding * 2 = 200 * 0.6 + 1 * 2 = 122
        Assert.assertEquals(
                "Drag shadow height not match. Should do resize for image and add 1dp border.",
                122,
                mDragAndDropDelegateImpl.getDragShadowHeight());
        Assert.assertNotNull(
                "Cached Image bytes should not be null.",
                mDropDataProviderImpl.getImageBytesForTesting());
        assertDragTypeNotRecorded("Drag didn't end.");

        DragEvent dragEnd = mockDragEvent(DragEvent.ACTION_DRAG_ENDED);
        mDragAndDropDelegateImpl.onDrag(mContainerView, dragEnd);
        Assert.assertNull(
                "Cached Image bytes should be cleaned.",
                mDropDataProviderImpl.getImageBytesForTesting());
        assertDragTypeRecorded(DragTargetType.IMAGE);
        assertDragOutsideWebContentHistogramsRecorded(/* dropResult= */ false);
    }

    @Test
    public void testStartDragAndDrop_TextLink() {
        final Bitmap shadowImage = Bitmap.createBitmap(100, 200, Bitmap.Config.ALPHA_8);
        final DropDataAndroid dropData =
                DropDataAndroid.create("text", JUnitTestGURLs.EXAMPLE_URL, null, null, null);

        mDragAndDropDelegateImpl.startDragAndDrop(
                mContainerView,
                shadowImage,
                dropData,
                mContainerView.getContext(),
                /* cursorOffsetX= */ 0,
                /* cursorOffsetY= */ 0,
                /* dragObjRectWidth= */ 100,
                /* dragObjRectHeight= */ 200);

        Assert.assertTrue("Drag should start.", mDragAndDropDelegateImpl.isDragStarted());
        Assert.assertEquals(
                "Drag shadow width does not match. Should not resize for text link.",
                100,
                mDragAndDropDelegateImpl.getDragShadowWidth());
        Assert.assertEquals(
                "Drag shadow height does not match. Should not resize for text link.",
                200,
                mDragAndDropDelegateImpl.getDragShadowHeight());
        assertDragTypeNotRecorded("Drag did not end.");

        mDragAndDropDelegateImpl.onDrag(mContainerView, mockDragEvent(DragEvent.ACTION_DRAG_ENDED));

        Assert.assertFalse("Drag should end.", mDragAndDropDelegateImpl.isDragStarted());
        Assert.assertEquals(
                "Drag shadow width should be reset.",
                0,
                mDragAndDropDelegateImpl.getDragShadowWidth());
        Assert.assertEquals(
                "Drag shadow height should be reset.",
                0,
                mDragAndDropDelegateImpl.getDragShadowHeight());
        assertDragTypeRecorded(DragTargetType.LINK);
        assertDragOutsideWebContentHistogramsRecorded(/* dropResult= */ false);
    }

    @Test
    public void testStartDragAndDrop_NotSupportedForA11y() {
        final Bitmap shadowImage = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
        final DropDataAndroid dropData = DropDataAndroid.create("text", null, null, null, null);

        Assert.assertTrue(
                "Drag and drop should start.",
                mDragAndDropDelegateImpl.startDragAndDrop(
                        mContainerView,
                        shadowImage,
                        dropData,
                        mContainerView.getContext(),
                        /* cursorOffsetX= */ 0,
                        /* cursorOffsetY= */ 0,
                        /* dragObjRectWidth= */ 100,
                        /* dragObjRectHeight= */ 200));

        AccessibilityState.setIsAnyAccessibilityServiceEnabledForTesting(true);
        Assert.assertFalse(
                "Drag and drop should not start when isAnyAccessibilityServiceEnabled=true.",
                mDragAndDropDelegateImpl.startDragAndDrop(
                        mContainerView,
                        shadowImage,
                        dropData,
                        mContainerView.getContext(),
                        /* cursorOffsetX= */ 0,
                        /* cursorOffsetY= */ 0,
                        /* dragObjRectWidth= */ 100,
                        /* dragObjRectHeight= */ 200));
    }

    @Test
    public void testStartDragAndDrop_InvalidDropData() {
        final DropDataAndroid dropData = DropDataAndroid.create(null, null, null, null, null);

        Assert.assertFalse(
                "Drag and drop should not start.",
                mDragAndDropDelegateImpl.startDragAndDrop(
                        mContainerView,
                        Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888),
                        dropData,
                        mContainerView.getContext(),
                        /* cursorOffsetX= */ 0,
                        /* cursorOffsetY= */ 0,
                        /* dragObjRectWidth= */ 100,
                        /* dragObjRectHeight= */ 200));
    }

    @Test
    public void testStartDragAndDrop_WithDragShadowBuilder() {
        final DropDataAndroid dropData = DropDataAndroid.create("text", null, null, null, null);
        DragShadowBuilder mockBuilder = mock(DragShadowBuilder.class);
        Assert.assertTrue(
                "Drag and drop should start.",
                mDragAndDropDelegateImpl.startDragAndDrop(mContainerView, mockBuilder, dropData));
        Assert.assertTrue("Drag should be started.", mDragAndDropDelegateImpl.isDragStarted());
    }

    @Test
    public void testDragImage_ShadowPlaceholder() {
        final Bitmap shadowImage = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
        final DropDataAndroid imageDropData =
                DropDataAndroid.create("", null, new byte[] {1, 2, 3, 4}, "png", IMAGE_FILENAME);
        mDragAndDropDelegateImpl.startDragAndDrop(
                mContainerView,
                shadowImage,
                imageDropData,
                mContainerView.getContext(),
                /* cursorOffsetX= */ 0,
                /* cursorOffsetY= */ 0,
                /* dragObjRectWidth= */ 100,
                /* dragObjRectHeight= */ 200);

        Assert.assertNotNull("LastDragShadowBuilder is null.", mLastDragShadowBuilder);
        View shadowView = mLastDragShadowBuilder.getView();
        Assert.assertTrue(
                "DrawShadowBuilder should host an ImageView.", shadowView instanceof ImageView);
        Assert.assertTrue(
                "Drag shadow image should host a globe icon, which should be a vector drawable.",
                ((ImageView) shadowView).getDrawable() instanceof VectorDrawable);
    }

    @Test
    public void testDragImage_DragHandled() {
        final Bitmap shadowImage = Bitmap.createBitmap(100, 200, Bitmap.Config.ALPHA_8);
        final DropDataAndroid imageDropData =
                DropDataAndroid.create("", null, new byte[] {1, 2, 3, 4}, "png", IMAGE_FILENAME);
        mDragAndDropDelegateImpl.startDragAndDrop(
                mContainerView,
                shadowImage,
                imageDropData,
                mContainerView.getContext(),
                /* cursorOffsetX= */ 0,
                /* cursorOffsetY= */ 0,
                /* dragObjRectWidth= */ 100,
                /* dragObjRectHeight= */ 200);

        final DragEvent dragEndEvent = mockDragEvent(DragEvent.ACTION_DRAG_ENDED);
        doReturn(true).when(dragEndEvent).getResult();
        mDragAndDropDelegateImpl.onDrag(mContainerView, dragEndEvent);

        Assert.assertNotNull(
                "Cached Image bytes should not be cleaned, drag is handled.",
                mDropDataProviderImpl.getImageBytesForTesting());
        assertDragTypeRecorded(DragTargetType.IMAGE);
        assertDragOutsideWebContentHistogramsRecorded(/* dropResult= */ true);
    }

    @Test
    @EnableFeatures({UiAndroidFeatureList.DRAG_DROP_FILES})
    public void testDragImage_ReceivedDropBeforeDragEnds() {
        final Bitmap shadowImage = Bitmap.createBitmap(100, 200, Bitmap.Config.ALPHA_8);
        final DropDataAndroid imageDropData =
                DropDataAndroid.create("", null, new byte[] {1, 2, 3, 4}, "png", IMAGE_FILENAME);
        mDragAndDropDelegateImpl.startDragAndDrop(
                mContainerView,
                shadowImage,
                imageDropData,
                mContainerView.getContext(),
                /* cursorOffsetX= */ 0,
                /* cursorOffsetY= */ 0,
                /* dragObjRectWidth= */ 100,
                /* dragObjRectHeight= */ 200);

        mDragAndDropDelegateImpl.onDrag(mContainerView, mockDragEvent(DragEvent.ACTION_DROP));
        final DragEvent dragEndEvent = mockDragEvent(DragEvent.ACTION_DRAG_ENDED);
        doReturn(true).when(dragEndEvent).getResult();
        mDragAndDropDelegateImpl.onDrag(mContainerView, dragEndEvent);

        // Drop on the same view does not lead to recording of drag duration.
        assertDragTypeNotRecorded("Drag dropped on the same view.");
        assertDropInWebContentHistogramsRecorded();
        Assert.assertNotNull(
                "Cached Image bytes should not be cleaned, drag is handled.",
                mDropDataProviderImpl.getImageBytesForTesting());
    }

    @Test
    public void testIgnoreDragStartedElsewhere() {
        mDragAndDropDelegateImpl.onDrag(mContainerView, mockDragEvent(DragEvent.ACTION_DROP));
        mDragAndDropDelegateImpl.onDrag(mContainerView, mockDragEvent(DragEvent.ACTION_DRAG_ENDED));

        assertDragTypeNotRecorded("Drag dropped on the same view.");
        assertHistogramRecorded(
                "Android.DragDrop.FromWebContent.DropInWebContent.Duration",
                false,
                "Only tracking drag started by mDragAndDropDelegateImpl#startDragAndDrop.");
        assertHistogramRecorded(
                "Android.DragDrop.FromWebContent.DropInWebContent.DistanceDip",
                false,
                "Only tracking drag started by mDragAndDropDelegateImpl#startDragAndDrop.");
    }

    @Test
    public void testDragStartedFromContainerView() {
        final Bitmap shadowImage = Bitmap.createBitmap(100, 200, Bitmap.Config.ALPHA_8);
        final DropDataAndroid imageDropData =
                DropDataAndroid.create("", null, new byte[] {1, 2, 3, 4}, "png", IMAGE_FILENAME);
        mDragAndDropDelegateImpl.startDragAndDrop(
                mContainerView,
                shadowImage,
                imageDropData,
                mContainerView.getContext(),
                /* cursorOffsetX= */ 0,
                /* cursorOffsetY= */ 0,
                /* dragObjRectWidth= */ 100,
                /* dragObjRectHeight= */ 200);

        mDragAndDropDelegateImpl.onDrag(
                mContainerView, mockDragEvent(DragEvent.ACTION_DRAG_STARTED));
        Assert.assertEquals(
                "Recorded drag start X dp should match.",
                DRAG_START_X_DP,
                mDragAndDropDelegateImpl.getDragStartXDp(),
                0.0f);
        Assert.assertEquals(
                "Recorded drag start Y dp should match.",
                DRAG_START_Y_DP,
                mDragAndDropDelegateImpl.getDragStartYDp(),
                0.0f);
    }

    @Test
    public void testTextForLinkData_UrlWithNoTitle() {
        final DropDataAndroid dropData =
                DropDataAndroid.create("", JUnitTestGURLs.EXAMPLE_URL, null, null, null);

        String text = DragAndDropDelegateImpl.getTextForLinkData(dropData);
        Assert.assertEquals("Text should match.", JUnitTestGURLs.EXAMPLE_URL.getSpec(), text);
    }

    @Test
    public void testTextForLinkData_UrlWithTitle() {
        String linkTitle = "Link text";
        final DropDataAndroid dropData =
                DropDataAndroid.create(linkTitle, JUnitTestGURLs.EXAMPLE_URL, null, null, null);

        String text = DragAndDropDelegateImpl.getTextForLinkData(dropData);
        Assert.assertEquals(
                "Text should match.",
                linkTitle + "\n" + JUnitTestGURLs.EXAMPLE_URL.getSpec(),
                text);
    }

    @Test
    public void testClipData_ImageWithUrl() {
        final DropDataAndroid dropData =
                DropDataAndroid.create(
                        "",
                        JUnitTestGURLs.EXAMPLE_URL,
                        new byte[] {1, 2, 3, 4},
                        "png",
                        IMAGE_FILENAME);

        ClipData clipData = mDragAndDropDelegateImpl.buildClipData(dropData);
        Assert.assertEquals(
                "Image ClipData should only include image.", 1, clipData.getItemCount());
        Assert.assertNotNull("Image Uri should exist.", clipData.getItemAt(0).getUri());
    }

    @Test
    public void testClipData_TextLink_NonNullIntent() {
        final DropDataAndroid dropData =
                DropDataAndroid.create("", JUnitTestGURLs.EXAMPLE_URL, null, null, null);
        mDragAndDropDelegateImpl.setDragAndDropBrowserDelegate(
                mockDragAndDropBrowserDelegate(false, false, null, new Intent()));
        ClipData clipData = mDragAndDropDelegateImpl.buildClipData(dropData);
        Assert.assertTrue(
                "Link ClipData should include plaintext MIME type.",
                clipData.getDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN));
        Assert.assertTrue(
                "Link ClipData should include intent MIME type.",
                clipData.getDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT));
        Assert.assertEquals(
                "Dragged link text should match.",
                JUnitTestGURLs.EXAMPLE_URL.getSpec(),
                clipData.getItemAt(0).getText());
        Assert.assertNotNull(
                "ClipData intent should not be null.", clipData.getItemAt(0).getIntent());
    }

    @Test
    public void testClipData_TextLink_NullIntent() {
        final DropDataAndroid dropData =
                DropDataAndroid.create("", JUnitTestGURLs.EXAMPLE_URL, null, null, null);
        mDragAndDropDelegateImpl.setDragAndDropBrowserDelegate(
                mockDragAndDropBrowserDelegate(false, false, null, null));
        ClipData clipData = mDragAndDropDelegateImpl.buildClipData(dropData);
        Assert.assertTrue(
                "Link ClipData should include plaintext MIME type.",
                clipData.getDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN));
        Assert.assertFalse(
                "Link ClipData should not include intent MIME type.",
                clipData.getDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT));
        Assert.assertEquals(
                "Dragged link text should match.",
                JUnitTestGURLs.EXAMPLE_URL.getSpec(),
                clipData.getItemAt(0).getText());
    }

    @Test
    public void testClipData_browserContent() {
        ClipData mockClipData = mock(ClipData.class);
        DropDataAndroid mockDropData = mock(DropDataAndroid.class);
        when(mockDropData.hasBrowserContent()).thenReturn(true);
        when(mDragAndDropBrowserDelegate.buildClipData(mockDropData)).thenReturn(mockClipData);
        mDragAndDropDelegateImpl.setDragAndDropBrowserDelegate(mDragAndDropBrowserDelegate);
        ClipData actualClipData = mDragAndDropDelegateImpl.buildClipData(mockDropData);
        Assert.assertEquals("ClipData is not as expected.", mockClipData, actualClipData);
        verify(mDragAndDropBrowserDelegate).buildClipData(mockDropData);
    }

    @Test
    public void testBuildFlag_Link() {
        final DropDataAndroid data =
                DropDataAndroid.create("", JUnitTestGURLs.EXAMPLE_URL, null, null, null);
        int flag = mDragAndDropDelegateImpl.buildFlags(data);
        Assert.assertEquals("Expect flag(s): DRAG_FLAG_GLOBAL.", flag, View.DRAG_FLAG_GLOBAL);
    }

    @Test
    public void testBuildFlag_Text() {
        final DropDataAndroid data = DropDataAndroid.create("text", null, null, null, null);
        int flag = mDragAndDropDelegateImpl.buildFlags(data);
        Assert.assertEquals("Expect flag(s): DRAG_FLAG_GLOBAL.", flag, View.DRAG_FLAG_GLOBAL);
    }

    @Test
    public void testBuildFlag_LinkText() {
        final DropDataAndroid data =
                DropDataAndroid.create("text", JUnitTestGURLs.EXAMPLE_URL, null, null, null);
        int flag = mDragAndDropDelegateImpl.buildFlags(data);
        Assert.assertEquals("Expect flag(s): DRAG_FLAG_GLOBAL.", flag, View.DRAG_FLAG_GLOBAL);
    }

    @Test
    public void testBuildFlag_Image() {
        mDragAndDropDelegateImpl.setDragAndDropBrowserDelegate(mDragAndDropBrowserDelegate);
        doReturn(true).when(mDragAndDropBrowserDelegate).getSupportAnimatedImageDragShadow();
        final DropDataAndroid imageData =
                DropDataAndroid.create("", null, new byte[] {1, 2, 3, 4}, "png", IMAGE_FILENAME);
        int flag = mDragAndDropDelegateImpl.buildFlags(imageData);
        Assert.assertEquals(
                "Expect flag(s): DRAG_FLAG_GLOBAL | DRAG_FLAG_GLOBAL_URI_READ | DRAG_FLAG_OPAQUE.",
                flag,
                View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ | View.DRAG_FLAG_OPAQUE);
    }

    @Test
    public void testBuildFlag_ImageLink() {
        final DropDataAndroid imageData =
                DropDataAndroid.create(
                        "",
                        JUnitTestGURLs.EXAMPLE_URL,
                        new byte[] {1, 2, 3, 4},
                        "png",
                        IMAGE_FILENAME);
        int flag = mDragAndDropDelegateImpl.buildFlags(imageData);
        Assert.assertEquals(
                "Expect flag(s): DRAG_FLAG_GLOBAL | DRAG_FLAG_GLOBAL_URI_READ.",
                flag,
                View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ);
    }

    @Test
    public void testBuildFlag_BrowserContent() {
        final DropDataAndroid browserData =
                new DropDataAndroid(null, null, null, null, null) {
                    @Override
                    public boolean hasBrowserContent() {
                        return true;
                    }
                };
        int flag = mDragAndDropDelegateImpl.buildFlags(browserData);
        Assert.assertEquals(
                "Expect flag(s): DRAG_FLAG_GLOBAL | DRAG_FLAG_OPAQUE.",
                flag,
                View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_OPAQUE);
    }

    @Test
    public void testBuildFlag_Invalid() {
        final DropDataAndroid browserData = new DropDataAndroid(null, null, null, null, null);
        Assert.assertEquals(
                "Invalid data will not have flag set.",
                0,
                mDragAndDropDelegateImpl.buildFlags(browserData));
    }

    @Test
    public void testDropInChromeFromOutside() {
        mDragAndDropDelegateImpl.setDragAndDropBrowserDelegate(
                mockDragAndDropBrowserDelegate(true, false, mDragAndDropPermissions, null));
        // Assume that data is dragged from outside Chrome.
        mDragAndDropDelegateImpl.onDrag(mContainerView, mockDragEvent(DragEvent.ACTION_DROP));
        verify(mDragAndDropPermissions).release();
    }

    private DragEvent mockDragEvent(int action) {
        DragEvent event = Mockito.mock(DragEvent.class);
        when(event.getX()).thenReturn(DRAG_START_X_DP);
        when(event.getY()).thenReturn(DRAG_START_Y_DP);
        doReturn(action).when(event).getAction();
        return event;
    }

    private void assertDragTypeNotRecorded(String reason) {
        assertHistogramRecorded("Android.DragDrop.FromWebContent.TargetType", false, reason);
    }

    private void assertDragTypeRecorded(@DragTargetType int type) {
        final String histogram = "Android.DragDrop.FromWebContent.TargetType";
        final String errorMsg = "<" + histogram + "> is not recorded correctly.";
        Assert.assertEquals(
                errorMsg, 1, RecordHistogram.getHistogramValueCountForTesting(histogram, type));
    }

    private void assertDragOutsideWebContentHistogramsRecorded(boolean dropResult) {
        // Verify drop outside metrics recorded.
        final String histogram =
                "Android.DragDrop.FromWebContent.Duration." + (dropResult ? "Success" : "Canceled");
        assertHistogramRecorded(histogram, true, "Drop outside of web content.");

        // Verify drop inside metrics not recorded.
        assertHistogramRecorded(
                "Android.DragDrop.FromWebContent.DropInWebContent.Duration",
                false,
                "Drop outside of web content.");
        assertHistogramRecorded(
                "Android.DragDrop.FromWebContent.DropInWebContent.DistanceDip",
                false,
                "Drop outside of web content.");
    }

    private void assertDropInWebContentHistogramsRecorded() {
        // Verify drop inside metrics recorded.
        assertHistogramRecorded(
                "Android.DragDrop.FromWebContent.DropInWebContent.Duration",
                true,
                "Drop inside web content.");
        assertHistogramRecorded(
                "Android.DragDrop.FromWebContent.DropInWebContent.DistanceDip",
                true,
                "Drop inside web content.");

        // Verify drop outside metrics not recorded.
        assertHistogramRecorded(
                "Android.DragDrop.FromWebContent.Duration.Success",
                false,
                "Should not recorded when drop inside web content.");
        assertHistogramRecorded(
                "Android.DragDrop.FromWebContent.Duration.Canceled",
                false,
                "Should not recorded when drop inside web content.");
    }

    private void assertHistogramRecorded(String histogram, boolean recorded, String reason) {
        Assert.assertEquals(
                String.format("<%s> is not recorded correctly. Reason: %s", histogram, reason),
                recorded ? 1 : 0,
                RecordHistogram.getHistogramTotalCountForTesting(histogram));
    }

    private DragAndDropBrowserDelegate mockDragAndDropBrowserDelegate(
            boolean supportDropInChrome,
            boolean supportAnimatedImageDragShadow,
            DragAndDropPermissions permissions,
            Intent intent) {
        when(mDragAndDropBrowserDelegate.getSupportDropInChrome()).thenReturn(supportDropInChrome);
        when(mDragAndDropBrowserDelegate.getSupportAnimatedImageDragShadow())
                .thenReturn(supportAnimatedImageDragShadow);
        when(mDragAndDropBrowserDelegate.getDragAndDropPermissions(any(DragEvent.class)))
                .thenReturn(permissions);
        when(mDragAndDropBrowserDelegate.createUrlIntent(anyString(), anyInt())).thenReturn(intent);
        return mDragAndDropBrowserDelegate;
    }
}