diff --git a/AnkiDroid/build.gradle b/AnkiDroid/build.gradle index abc005a5ad29..8e2e05a49412 100644 --- a/AnkiDroid/build.gradle +++ b/AnkiDroid/build.gradle @@ -337,6 +337,7 @@ dependencies { implementation libs.androidx.appcompat implementation libs.androidx.browser implementation libs.androidx.core.ktx + implementation libs.androidx.draganddrop implementation libs.androidx.exifinterface implementation libs.androidx.fragment.ktx implementation libs.androidx.media diff --git a/AnkiDroid/src/main/AndroidManifest.xml b/AnkiDroid/src/main/AndroidManifest.xml index 075cdf87aecb..84b1ba438b92 100644 --- a/AnkiDroid/src/main/AndroidManifest.xml +++ b/AnkiDroid/src/main/AndroidManifest.xml @@ -21,6 +21,10 @@ + + + + diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditText.kt b/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditText.kt index 8a14b79ca723..400b55a4a269 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditText.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditText.kt @@ -16,6 +16,7 @@ package com.ichi2.anki +import android.content.ClipDescription import android.content.ClipboardManager import android.content.Context import android.graphics.drawable.Drawable @@ -25,25 +26,18 @@ import android.os.LocaleList import android.os.Parcelable import android.text.InputType import android.util.AttributeSet -import android.view.View import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputConnection import android.widget.EditText import androidx.annotation.RequiresApi import androidx.annotation.VisibleForTesting -import androidx.core.view.ContentInfoCompat -import androidx.core.view.OnReceiveContentListener -import androidx.core.view.ViewCompat -import androidx.core.view.inputmethod.EditorInfoCompat -import androidx.core.view.inputmethod.InputConnectionCompat import com.google.android.material.color.MaterialColors import com.ichi2.anki.preferences.sharedPrefs import com.ichi2.anki.servicelayer.NoteService import com.ichi2.ui.FixedEditText -import com.ichi2.utils.ClipboardUtil.IMAGE_MIME_TYPES -import com.ichi2.utils.ClipboardUtil.getImageUri +import com.ichi2.utils.ClipboardUtil.getDescription import com.ichi2.utils.ClipboardUtil.getPlainText -import com.ichi2.utils.ClipboardUtil.hasImage +import com.ichi2.utils.ClipboardUtil.getUri +import com.ichi2.utils.ClipboardUtil.hasMedia import com.ichi2.utils.KotlinCleanup import kotlinx.parcelize.Parcelize import timber.log.Timber @@ -55,7 +49,7 @@ class FieldEditText : FixedEditText, NoteService.NoteField { override var ord = 0 private var origBackground: Drawable? = null private var selectionChangeListener: TextSelectionListener? = null - private var imageListener: ImagePasteListener? = null + private var pasteListener: PasteListener? = null @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) var clipboard: ClipboardManager? = null @@ -96,51 +90,8 @@ class FieldEditText : FixedEditText, NoteService.NoteField { setDefaultStyle() } - fun setImagePasteListener(imageListener: ImagePasteListener?) { - this.imageListener = imageListener - } - - @KotlinCleanup("add extension method to iterate clip items") - override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? { - val inputConnection = super.onCreateInputConnection(editorInfo) ?: return null - EditorInfoCompat.setContentMimeTypes(editorInfo, IMAGE_MIME_TYPES) - ViewCompat.setOnReceiveContentListener( - this, - IMAGE_MIME_TYPES, - object : OnReceiveContentListener { - override fun onReceiveContent(view: View, payload: ContentInfoCompat): ContentInfoCompat? { - val pair = payload.partition { item -> item.uri != null } - val uriContent = pair.first - val remaining = pair.second - - if (imageListener == null || uriContent == null) { - return remaining - } - - val clip = uriContent.clip - val description = clip.description - - if (!hasImage(description)) { - return remaining - } - - for (i in 0 until clip.itemCount) { - val uri = clip.getItemAt(i).uri - try { - onImagePaste(uri) - } catch (e: Exception) { - Timber.w(e) - CrashReportService.sendExceptionReport(e, "NoteEditor::onImage") - return remaining - } - } - - return remaining - } - } - ) - - return InputConnectionCompat.createWrapper(this, inputConnection, editorInfo) + fun setPasteListener(pasteListener: PasteListener?) { + this.pasteListener = pasteListener } override fun onSelectionChanged(selStart: Int, selEnd: Int) { @@ -193,8 +144,8 @@ class FieldEditText : FixedEditText, NoteService.NoteField { override fun onTextContextMenuItem(id: Int): Boolean { // This handles both CTRL+V and "Paste" if (id == android.R.id.paste) { - if (hasImage(clipboard)) { - return onImagePaste(getImageUri(clipboard)) + if (hasMedia(clipboard)) { + return onPaste(getUri(clipboard), getDescription(clipboard)) } return pastePlainText() } @@ -215,11 +166,11 @@ class FieldEditText : FixedEditText, NoteService.NoteField { return false } - private fun onImagePaste(imageUri: Uri?): Boolean { - return if (imageUri == null) { + private fun onPaste(uri: Uri?, description: ClipDescription?): Boolean { + return if (uri == null) { false } else { - imageListener!!.onImagePaste(this, imageUri) + pasteListener!!.onPaste(this, uri, description) } } @@ -251,8 +202,8 @@ class FieldEditText : FixedEditText, NoteService.NoteField { fun onSelectionChanged(selStart: Int, selEnd: Int) } - fun interface ImagePasteListener { - fun onImagePaste(editText: EditText, uri: Uri?): Boolean + fun interface PasteListener { + fun onPaste(editText: EditText, uri: Uri?, description: ClipDescription?): Boolean } companion object { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/MediaRegistration.kt b/AnkiDroid/src/main/java/com/ichi2/anki/MediaRegistration.kt index 2e672b51796f..4672e8c91f60 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/MediaRegistration.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/MediaRegistration.kt @@ -15,14 +15,17 @@ */ package com.ichi2.anki +import android.content.ClipDescription import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import androidx.annotation.CheckResult import com.ichi2.anki.multimediacard.fields.ImageField +import com.ichi2.anki.multimediacard.fields.MediaClipField import com.ichi2.compat.CompatHelper import com.ichi2.libanki.exception.EmptyMediaException +import com.ichi2.utils.ClipboardUtil import com.ichi2.utils.ContentResolverUtil.getFileName import com.ichi2.utils.FileUtil.getFileNameAndExtension import timber.log.Timber @@ -39,15 +42,15 @@ import java.io.InputStream */ class MediaRegistration(private val context: Context) { // Use the same HTML if the same image is pasted multiple times. - private val pastedImageCache = HashMap() + private val pastedMediaCache = HashMap() /** - * Loads an image into the collection.media directory and returns a HTML reference + * Loads an media into the collection.media directory and returns a HTML reference * @param uri The uri of the image to load * @return HTML referring to the loaded image */ @Throws(IOException::class) - fun loadImageIntoCollection(uri: Uri): String? { + fun loadMediaIntoCollection(uri: Uri, description: ClipDescription): String? { val fileName: String val filename = getFileName(context.contentResolver, uri) val fd = openInputStreamWithURI(uri) @@ -59,9 +62,12 @@ class MediaRegistration(private val context: Context) { } var clipCopy: File var bytesWritten: Long + val isImage = ClipboardUtil.hasImage(description) + val isSVG = ClipboardUtil.hasSVG(description) + openInputStreamWithURI(uri).use { copyFd -> // no conversion to jpg in cases of gif and jpg and if png image with alpha channel - if (shouldConvertToJPG(fileNameAndExtension.value, copyFd)) { + if (!isSVG && isImage && shouldConvertToJPG(fileNameAndExtension.value, copyFd)) { clipCopy = File.createTempFile(fileName, ".jpg") bytesWritten = CompatHelper.compat.copyFile(fd, clipCopy.absolutePath) // return null if jpg conversion false. @@ -81,11 +87,15 @@ class MediaRegistration(private val context: Context) { Timber.d("File was %d bytes", bytesWritten) if (bytesWritten > MEDIA_MAX_SIZE) { Timber.w("File was too large: %d bytes", bytesWritten) - showThemedToast(context, context.getString(R.string.note_editor_paste_too_large), false) + showThemedToast(context, context.getString(R.string.note_editor_media_too_large), false) File(tempFilePath).delete() return null } - val field = ImageField() + val field = if (isImage) { + ImageField() + } else { + MediaClipField() + } field.hasTemporaryMedia = true field.mediaPath = tempFilePath return field.formattedValue @@ -129,13 +139,13 @@ class MediaRegistration(private val context: Context) { return fileNameAndExtension.key.length <= 3 } - fun onImagePaste(uri: Uri): String? { + fun onPaste(uri: Uri, description: ClipDescription): String? { return try { // check if cache already holds registered file or not - if (!pastedImageCache.containsKey(uri.toString())) { - pastedImageCache[uri.toString()] = loadImageIntoCollection(uri) + if (!pastedMediaCache.containsKey(uri.toString())) { + pastedMediaCache[uri.toString()] = loadMediaIntoCollection(uri, description) } - pastedImageCache[uri.toString()] + pastedMediaCache[uri.toString()] } catch (ex: NullPointerException) { // Tested under FB Messenger and GMail, both apps do nothing if this occurs. // This typically works if the user copies again - don't know the exact cause @@ -143,28 +153,28 @@ class MediaRegistration(private val context: Context) { // java.lang.SecurityException: Permission Denial: opening provider // org.chromium.chrome.browser.util.ChromeFileProvider from ProcessRecord{80125c 11262:com.ichi2.anki/u0a455} // (pid=11262, uid=10455) that is not exported from UID 10057 - Timber.w(ex, "Failed to paste image") + Timber.w(ex, "Failed to paste media") null } catch (ex: SecurityException) { - Timber.w(ex, "Failed to paste image") + Timber.w(ex, "Failed to paste media") null } catch (e: Exception) { // NOTE: This is happy path coding which works on Android 9. CrashReportService.sendExceptionReport("File is invalid issue:8880", "RegisterMediaForWebView:onImagePaste URI of file:$uri") - Timber.w(e, "Failed to paste image") + Timber.w(e, "Failed to paste media") showThemedToast(context, context.getString(R.string.multimedia_editor_something_wrong), false) null } } @CheckResult - fun registerMediaForWebView(imagePath: String?): Boolean { - if (imagePath == null) { + fun registerMediaForWebView(mediaPath: String?): Boolean { + if (mediaPath == null) { // Nothing to register - continue with execution. return true } - Timber.i("Adding media to collection: %s", imagePath) - val f = File(imagePath) + Timber.i("Adding media to collection: %s", mediaPath) + val f = File(mediaPath) return try { CollectionManager.getColUnsafe().media.addFile(f) true diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt index f9bfad2230cd..db1bd168e173 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt @@ -23,6 +23,7 @@ import android.app.Activity import android.app.Activity.RESULT_CANCELED import android.content.BroadcastReceiver import android.content.ClipData +import android.content.ClipDescription import android.content.ClipboardManager import android.content.Context import android.content.Intent @@ -68,6 +69,9 @@ import androidx.core.content.edit import androidx.core.content.res.ResourcesCompat import androidx.core.os.BundleCompat import androidx.core.text.HtmlCompat +import androidx.core.util.component1 +import androidx.core.util.component2 +import androidx.core.view.OnReceiveContentListener import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope @@ -128,6 +132,8 @@ import com.ichi2.anki.widgets.DeckDropDownAdapter.SubtitleListener import com.ichi2.annotations.NeedsTest import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat import com.ichi2.compat.CompatHelper.Companion.registerReceiverCompat +import com.ichi2.compat.DropHelperCompat +import com.ichi2.compat.DropHelperOptionsBuilder import com.ichi2.libanki.Card import com.ichi2.libanki.Collection import com.ichi2.libanki.Consts @@ -145,6 +151,8 @@ import com.ichi2.libanki.note import com.ichi2.libanki.undoableOp import com.ichi2.utils.AdaptionUtil import com.ichi2.utils.ClipboardUtil +import com.ichi2.utils.ClipboardUtil.hasMedia +import com.ichi2.utils.ClipboardUtil.items import com.ichi2.utils.HashUtil import com.ichi2.utils.ImageUtils import com.ichi2.utils.ImportUtils @@ -172,7 +180,6 @@ import java.io.File import java.util.LinkedList import java.util.Locale import java.util.function.Consumer -import kotlin.collections.ArrayList import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt @@ -371,6 +378,37 @@ class NoteEditor : AnkiFragment(R.layout.note_editor), DeckSelectionListener, Su } ) + /** + * Listener for handling content received via drag and drop or copy and paste. + * This listener processes URIs contained in the payload and attempts to paste the content into the target EditText view. + */ + private val onReceiveContentListener = OnReceiveContentListener { view, payload -> + val (uriContent, remaining) = payload.partition { item -> item.uri != null } + + if (uriContent == null) { + return@OnReceiveContentListener remaining + } + + val clip = uriContent.clip + val description = clip.description + + if (!hasMedia(description)) { + return@OnReceiveContentListener remaining + } + + for (uri in clip.items().map { it.uri }) { + try { + onPaste(view as EditText, uri, description) + } catch (e: Exception) { + Timber.w(e) + CrashReportService.sendExceptionReport(e, "NoteEditor::onReceiveContent") + return@OnReceiveContentListener remaining + } + } + + return@OnReceiveContentListener remaining + } + private inner class NoteEditorActivityResultCallback(private val callback: (result: ActivityResult) -> Unit) : ActivityResultCallback { override fun onActivityResult(result: ActivityResult) { Timber.d("onActivityResult() with result: %s", result.resultCode) @@ -659,7 +697,7 @@ class NoteEditor : AnkiFragment(R.layout.note_editor), DeckSelectionListener, Su // TODO: Support all extensions // See https://github.com/ankitects/anki/blob/6f3550464d37aee1b8b784e431cbfce8382d3ce7/rslib/src/image_occlusion/imagedata.rs#L154 if (ClipboardUtil.hasImage(clipboard)) { - val uri = ClipboardUtil.getImageUri(clipboard) + val uri = ClipboardUtil.getUri(clipboard) val i = Intent().apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) clipData = ClipData.newUri(requireActivity().contentResolver, uri.toString(), uri) @@ -1625,12 +1663,23 @@ class NoteEditor : AnkiFragment(R.layout.note_editor), DeckSelectionListener, Su val editLineView = editLines[i] customViewIds.add(editLineView.id) val newEditText = editLineView.editText - newEditText.setImagePasteListener { editText: EditText?, uri: Uri? -> - onImagePaste( + newEditText.setPasteListener { editText: EditText?, uri: Uri?, description: ClipDescription? -> + onPaste( editText!!, - uri!! + uri!!, + description!! ) } + DropHelperCompat.configureView( + requireActivity(), + editLineView, + DropHelperOptionsBuilder() + .setHighlightColor(R.color.material_lime_green_A700) + .setHighlightCornerRadiusPx(0) + .addInnerEditTexts(newEditText) + .build(), + onReceiveContentListener + ) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { if (i == 0) { findViewById(R.id.note_deck_spinner).nextFocusForwardId = newEditText.id @@ -1825,8 +1874,8 @@ class NoteEditor : AnkiFragment(R.layout.note_editor), DeckSelectionListener, Su } } - private fun onImagePaste(editText: EditText, uri: Uri): Boolean { - val imageTag = mediaRegistration!!.onImagePaste(uri) ?: return false + private fun onPaste(editText: EditText, uri: Uri, description: ClipDescription): Boolean { + val imageTag = mediaRegistration!!.onPaste(uri, description) ?: return false insertStringInField(editText, imageTag) return true } diff --git a/AnkiDroid/src/main/java/com/ichi2/compat/DropHelperCompat.kt b/AnkiDroid/src/main/java/com/ichi2/compat/DropHelperCompat.kt new file mode 100644 index 000000000000..ee86b4cdd04e --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/compat/DropHelperCompat.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +package com.ichi2.compat + +import android.app.Activity +import android.os.Build +import android.view.View +import androidx.core.view.OnReceiveContentListener +import androidx.draganddrop.DropHelper +import com.ichi2.utils.ClipboardUtil.MEDIA_MIME_TYPES + +typealias DropHelperOptionsCompat = DropHelper.Options +typealias DropHelperOptionsBuilder = DropHelper.Options.Builder + +/** + * We have applied `tools:overrideLibrary="androidx.draganddrop"` so we manually need to handle + * compat of [DropHelper] + */ +object DropHelperCompat { + + /** + * Configures a [View] for drag and drop operations, including the highlighting that + * indicates the view is a drop target. Sets a listener that enables the view to handle dropped + * data. + * + * @see DropHelper.configureView + */ + fun configureView( + activity: Activity, + view: View, + options: DropHelperOptionsCompat, + onReceiveContentListener: OnReceiveContentListener + ) { + // library will fail < API 24 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return + + DropHelper.configureView( + activity, + view, + MEDIA_MIME_TYPES, + options, + onReceiveContentListener + ) + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/ClipboardUtil.kt b/AnkiDroid/src/main/java/com/ichi2/utils/ClipboardUtil.kt index eb61b9e16a92..e3cd05412e60 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/ClipboardUtil.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/ClipboardUtil.kt @@ -32,12 +32,13 @@ import timber.log.Timber object ClipboardUtil { // JPEG is sent via pasted content - val IMAGE_MIME_TYPES = arrayOf("image/gif", "image/png", "image/jpg", "image/jpeg") + val IMAGE_MIME_TYPES = arrayOf("image/*") + val AUDIO_MIME_TYPES = arrayOf("audio/*") + val VIDEO_MIME_TYPES = arrayOf("video/*") + val MEDIA_MIME_TYPES = arrayOf(*IMAGE_MIME_TYPES, *AUDIO_MIME_TYPES, *VIDEO_MIME_TYPES) fun hasImage(clipboard: ClipboardManager?): Boolean { - return clipboard - ?.takeIf { it.hasPrimaryClip() } - ?.primaryClip + return clipboard?.primaryClip ?.let { hasImage(it.description) } ?: false } @@ -48,16 +49,43 @@ object ClipboardUtil { ?: false } - private fun getFirstItem(clipboard: ClipboardManager?) = clipboard - ?.takeIf { it.hasPrimaryClip() } - ?.primaryClip + fun hasSVG(description: ClipDescription): Boolean { + return description.hasMimeType("image/svg+xml") + } + + fun hasMedia(clipboard: ClipboardManager?): Boolean { + return clipboard?.primaryClip + ?.let { hasMedia(it.description) } + ?: false + } + + fun hasMedia(description: ClipDescription?): Boolean { + return description + ?.run { MEDIA_MIME_TYPES.any { hasMimeType(it) } } + ?: false + } + + fun ClipData.items() = sequence { + for (j in 0 until itemCount) { + yield(getItemAt(j)) + } + } + + private fun getFirstItem(clipboard: ClipboardManager?) = clipboard?.primaryClip ?.takeIf { it.itemCount > 0 } ?.getItemAt(0) - fun getImageUri(clipboard: ClipboardManager?): Uri? { + fun getUri(clipboard: ClipboardManager?): Uri? { return getFirstItem(clipboard)?.uri } + fun getDescription(clipboard: ClipboardManager?): ClipDescription? { + return clipboard + ?.takeIf { it.hasPrimaryClip() } + ?.primaryClip + ?.description + } + @CheckResult fun getText(clipboard: ClipboardManager?): CharSequence? { return getFirstItem(clipboard)?.text diff --git a/AnkiDroid/src/main/res/values/02-strings.xml b/AnkiDroid/src/main/res/values/02-strings.xml index 473e0fc16a9d..9dcadf7bf379 100644 --- a/AnkiDroid/src/main/res/values/02-strings.xml +++ b/AnkiDroid/src/main/res/values/02-strings.xml @@ -240,7 +240,7 @@ Enter HTML to be inserted before and after the selected text\n\nLong press a toolbar item to edit or remove it Remove Toolbar Item? - The image is too large to paste, please insert the image manually + The media is too large, please insert the media manually diff --git a/AnkiDroid/src/test/java/com/ichi2/utils/ClipboardUtilTest.kt b/AnkiDroid/src/test/java/com/ichi2/utils/ClipboardUtilTest.kt index 26c636c4583e..17c31d358240 100644 --- a/AnkiDroid/src/test/java/com/ichi2/utils/ClipboardUtilTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/utils/ClipboardUtilTest.kt @@ -2,13 +2,33 @@ package com.ichi2.utils +import android.content.ClipData import android.content.ClipDescription import android.content.ClipboardManager +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.ichi2.utils.ClipboardUtil.AUDIO_MIME_TYPES +import com.ichi2.utils.ClipboardUtil.IMAGE_MIME_TYPES +import com.ichi2.utils.ClipboardUtil.VIDEO_MIME_TYPES import com.ichi2.utils.ClipboardUtil.hasImage +import com.ichi2.utils.ClipboardUtil.hasMedia import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith +@RunWith(AndroidJUnit4::class) class ClipboardUtilTest { + + private lateinit var clipboardManager: ClipboardManager + + @Before + fun setUp() { + clipboardManager = ApplicationProvider.getApplicationContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + } + @Test fun hasImageClipboardManagerNullTest() { val clipboardManager: ClipboardManager? = null @@ -20,4 +40,40 @@ class ClipboardUtilTest { val clipDescription: ClipDescription? = null assertFalse(hasImage(clipDescription)) } + + @Test + fun hasMediaClipboardManagerNullTest() { + val clipboardManager: ClipboardManager? = null + assertFalse(hasMedia(clipboardManager)) + } + + @Test + fun hasMediaDescriptionNullTest() { + val clipDescription: ClipDescription? = null + assertFalse(hasMedia(clipDescription)) + } + + @Test + fun hasMediaWithImageMimeTypeTest() { + val clipDescription = ClipDescription("label", IMAGE_MIME_TYPES) + val clipData = ClipData(clipDescription, ClipData.Item("image data")) + clipboardManager.setPrimaryClip(clipData) + assertTrue(hasMedia(clipboardManager)) + } + + @Test + fun hasMediaWithAudioMimeTypeTest() { + val clipDescription = ClipDescription("label", AUDIO_MIME_TYPES) + val clipData = ClipData(clipDescription, ClipData.Item("audio data")) + clipboardManager.setPrimaryClip(clipData) + assertTrue(hasMedia(clipboardManager)) + } + + @Test + fun hasMediaWithVideoMimeTypeTest() { + val clipDescription = ClipDescription("label", VIDEO_MIME_TYPES) + val clipData = ClipData(clipDescription, ClipData.Item("video data")) + clipboardManager.setPrimaryClip(clipData) + assertTrue(hasMedia(clipboardManager)) + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 38c3af7a1ad7..7ca6513ac80c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ androidxAppCompat = "1.7.0" androidxBrowser = "1.8.0" androidxConstraintlayout = "2.1.4" androidxCoreKtx = "1.13.1" +androidxDragAndDrop = "1.0.0" androidxExifinterface = "1.3.7" androidxFragmentKtx = "1.8.1" androidxImageCropper = "4.5.0" @@ -83,6 +84,7 @@ androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayo androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidxFragmentKtx" } androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "androidxExifinterface" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCoreKtx" } +androidx-draganddrop = { module = "androidx.draganddrop:draganddrop", version.ref = "androidxDragAndDrop" } androidx-browser = { module = "androidx.browser:browser", version.ref = "androidxBrowser" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidxAppCompat" } androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidxAnnotation" }