diff --git a/AnkiDroid/build.gradle b/AnkiDroid/build.gradle index 9d9009bab331..6f6199380d04 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 414e15995574..7819613e76dc 100644 --- a/AnkiDroid/src/main/AndroidManifest.xml +++ b/AnkiDroid/src/main/AndroidManifest.xml @@ -21,6 +21,12 @@ + + + + + + diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditText.kt b/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditText.kt index cea4773b0cdf..40d94e7fc359 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.getDescription import com.ichi2.utils.ClipboardUtil.getPlainText import com.ichi2.utils.ClipboardUtil.getUri -import com.ichi2.utils.ClipboardUtil.hasImage +import com.ichi2.utils.ClipboardUtil.hasMedia import com.ichi2.utils.KotlinCleanup import kotlinx.parcelize.Parcelize import timber.log.Timber @@ -100,49 +94,6 @@ class FieldEditText : FixedEditText, NoteService.NoteField { this.pasteListener = pasteListener } - @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 (pasteListener == 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 { - onPaste(uri) - } catch (e: Exception) { - Timber.w(e) - CrashReportService.sendExceptionReport(e, "NoteEditor::onImage") - return remaining - } - } - - return remaining - } - } - ) - - return InputConnectionCompat.createWrapper(this, inputConnection, editorInfo) - } - override fun onSelectionChanged(selStart: Int, selEnd: Int) { if (selectionChangeListener != null) { try { @@ -192,9 +143,10 @@ class FieldEditText : FixedEditText, NoteService.NoteField { override fun onTextContextMenuItem(id: Int): Boolean { // The current function is called both by Ctrl+V and pasting from the context menu + // It does not deal with drag and drop if (id == android.R.id.paste) { - if (hasImage(clipboard)) { - return onPaste(getUri(clipboard)) + if (hasMedia(clipboard)) { + return onPaste(getUri(clipboard), getDescription(clipboard)) } return pastePlainText() } @@ -215,11 +167,11 @@ class FieldEditText : FixedEditText, NoteService.NoteField { return false } - private fun onPaste(mediaUri: Uri?): Boolean { + private fun onPaste(mediaUri: Uri?, description: ClipDescription?): Boolean { return if (mediaUri == null) { false } else { - pasteListener!!.onPaste(this, mediaUri) + pasteListener!!.onPaste(this, mediaUri, description) } } @@ -252,7 +204,7 @@ class FieldEditText : FixedEditText, NoteService.NoteField { } fun interface PasteListener { - fun onPaste(editText: EditText, uri: Uri?): Boolean + 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 f27372219687..2ab46df5b963 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 @@ -47,7 +50,7 @@ class MediaRegistration(private val context: Context) { * @return HTML referring to the loaded image */ @Throws(IOException::class) - fun loadMediaIntoCollection(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 isVideo = ClipboardUtil.hasVideo(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 (shouldConvertToJPG(fileNameAndExtension.value, copyFd, isImage)) { clipCopy = File.createTempFile(fileName, ".jpg") bytesWritten = CompatHelper.compat.copyFile(fd, clipCopy.absolutePath) // return null if jpg conversion false. @@ -81,11 +87,22 @@ 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) + val message = if (isImage) { + context.getString(R.string.note_editor_image_too_large) + } else if (isVideo) { + context.getString(R.string.note_editor_video_too_large) + } else { + context.getString(R.string.note_editor_audio_too_large) + } + showThemedToast(context, message, 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 @@ -112,7 +129,13 @@ class MediaRegistration(private val context: Context) { return true // successful conversion to jpg. } - private fun shouldConvertToJPG(fileNameExtension: String, fileStream: InputStream): Boolean { + private fun shouldConvertToJPG(fileNameExtension: String, fileStream: InputStream, isImage: Boolean): Boolean { + if (!isImage) { + return false + } + if (".svg" == fileNameExtension) { + return false + } if (".jpg" == fileNameExtension) { return false // we are already a jpg, no conversion } @@ -129,11 +152,11 @@ class MediaRegistration(private val context: Context) { return fileNameAndExtension.key.length <= 3 } - fun onPaste(uri: Uri): String? { + fun onPaste(uri: Uri, description: ClipDescription): String? { return try { // check if cache already holds registered file or not if (!pastedMediaCache.containsKey(uri.toString())) { - pastedMediaCache[uri.toString()] = loadMediaIntoCollection(uri) + pastedMediaCache[uri.toString()] = loadMediaIntoCollection(uri, description) } pastedMediaCache[uri.toString()] } catch (ex: NullPointerException) { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt index e7b09accbdaa..68be362d572b 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 @@ -67,7 +68,11 @@ 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.draganddrop.DropHelper import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import anki.config.ConfigKey @@ -126,6 +131,7 @@ import com.ichi2.anki.utils.ext.isImageOcclusion import com.ichi2.anki.utils.ext.sharedPrefs import com.ichi2.anki.widgets.DeckDropDownAdapter.SubtitleListener import com.ichi2.annotations.NeedsTest +import com.ichi2.compat.CompatHelper import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat import com.ichi2.compat.CompatHelper.Companion.registerReceiverCompat import com.ichi2.libanki.Card @@ -144,6 +150,8 @@ import com.ichi2.libanki.load import com.ichi2.libanki.note import com.ichi2.libanki.undoableOp 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 @@ -169,7 +177,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 @@ -333,6 +340,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) @@ -1592,12 +1630,23 @@ class NoteEditor : AnkiFragment(R.layout.note_editor), DeckSelectionListener, Su val editLineView = editLines[i] customViewIds.add(editLineView.id) val newEditText = editLineView.editText - newEditText.setPasteListener { editText: EditText?, uri: Uri? -> + newEditText.setPasteListener { editText: EditText?, uri: Uri?, description: ClipDescription? -> onPaste( editText!!, - uri!! + uri!!, + description!! ) } + CompatHelper.compat.configureView( + requireActivity(), + editLineView, + DropHelper.Options.Builder() + .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 @@ -1835,8 +1884,8 @@ class NoteEditor : AnkiFragment(R.layout.note_editor), DeckSelectionListener, Su } } - private fun onPaste(editText: EditText, uri: Uri): Boolean { - val mediaTag = mediaRegistration!!.onPaste(uri) ?: return false + private fun onPaste(editText: EditText, uri: Uri, description: ClipDescription): Boolean { + val mediaTag = mediaRegistration!!.onPaste(uri, description) ?: return false insertStringInField(editText, mediaTag) return true } diff --git a/AnkiDroid/src/main/java/com/ichi2/compat/Compat.kt b/AnkiDroid/src/main/java/com/ichi2/compat/Compat.kt index 01708d410a36..dd8414fe8d16 100644 --- a/AnkiDroid/src/main/java/com/ichi2/compat/Compat.kt +++ b/AnkiDroid/src/main/java/com/ichi2/compat/Compat.kt @@ -17,6 +17,7 @@ package com.ichi2.compat +import android.app.Activity import android.content.Context import android.content.Intent import android.content.pm.PackageInfo @@ -30,6 +31,8 @@ import android.net.Uri import android.os.Bundle import android.view.View import androidx.annotation.CheckResult +import androidx.core.view.OnReceiveContentListener +import androidx.draganddrop.DropHelper import java.io.File import java.io.FileNotFoundException import java.io.IOException @@ -201,6 +204,19 @@ interface Compat { @Throws(IOException::class) fun contentOfDirectory(directory: File): FileStream + /** + * If possible, configures a [View] for drag and drop operations, including 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: DropHelper.Options, + onReceiveContentListener: OnReceiveContentListener + ) + /** * Converts a locale to a 'two letter' code (ISO-639-1 + ISO 3166-1 alpha-2) * Locale("spa", "MEX", "001") => Locale("es", "MX", "001") diff --git a/AnkiDroid/src/main/java/com/ichi2/compat/CompatV23.kt b/AnkiDroid/src/main/java/com/ichi2/compat/CompatV23.kt index 264e257ba6a3..9112fbbf48a1 100644 --- a/AnkiDroid/src/main/java/com/ichi2/compat/CompatV23.kt +++ b/AnkiDroid/src/main/java/com/ichi2/compat/CompatV23.kt @@ -16,6 +16,7 @@ package com.ichi2.compat +import android.app.Activity import android.content.Context import android.content.Intent import android.content.pm.PackageInfo @@ -31,6 +32,8 @@ import android.os.Vibrator import android.provider.MediaStore import android.view.View import androidx.appcompat.widget.TooltipCompat +import androidx.core.view.OnReceiveContentListener +import androidx.draganddrop.DropHelper import com.ichi2.utils.KotlinCleanup import timber.log.Timber import java.io.File @@ -147,6 +150,16 @@ open class CompatV23 : Compat { } } + // Until API 24 + override fun configureView( + activity: Activity, + view: View, + options: DropHelper.Options, + onReceiveContentListener: OnReceiveContentListener + ) { + // No implementation possible. + } + // Until API 26 @Throws(IOException::class) override fun deleteFile(file: File) { diff --git a/AnkiDroid/src/main/java/com/ichi2/compat/CompatV24.kt b/AnkiDroid/src/main/java/com/ichi2/compat/CompatV24.kt index 1ee05b2cc627..4edcf131c034 100644 --- a/AnkiDroid/src/main/java/com/ichi2/compat/CompatV24.kt +++ b/AnkiDroid/src/main/java/com/ichi2/compat/CompatV24.kt @@ -17,9 +17,14 @@ package com.ichi2.compat import android.annotation.TargetApi +import android.app.Activity import android.icu.util.ULocale import android.view.MotionEvent +import android.view.View +import androidx.core.view.OnReceiveContentListener +import androidx.draganddrop.DropHelper import com.ichi2.anki.common.utils.android.isRobolectric +import com.ichi2.utils.ClipboardUtil.MEDIA_MIME_TYPES import timber.log.Timber import java.util.Locale @@ -40,6 +45,21 @@ open class CompatV24 : CompatV23(), Compat { } } + override fun configureView( + activity: Activity, + view: View, + options: DropHelper.Options, + onReceiveContentListener: OnReceiveContentListener + ) { + DropHelper.configureView( + activity, + view, + MEDIA_MIME_TYPES, + options, + onReceiveContentListener + ) + } + override val AXIS_RELATIVE_X: Int = MotionEvent.AXIS_RELATIVE_X override val AXIS_RELATIVE_Y: Int = MotionEvent.AXIS_RELATIVE_Y } diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/ClipboardUtil.kt b/AnkiDroid/src/main/java/com/ichi2/utils/ClipboardUtil.kt index 7e3e462b2531..8b515799c106 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/ClipboardUtil.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/ClipboardUtil.kt @@ -31,13 +31,13 @@ import com.ichi2.anki.snackbar.showSnackbar 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,24 +48,53 @@ object ClipboardUtil { ?: false } - private fun getFirstItem(clipboard: ClipboardManager?) = clipboard - ?.takeIf { it.hasPrimaryClip() } - ?.primaryClip - ?.takeIf { it.itemCount > 0 } - ?.getItemAt(0) + fun hasVideo(description: ClipDescription?): Boolean { + return description + ?.run { VIDEO_MIME_TYPES.any { hasMimeType(it) } } + ?: false + } + + private fun ClipboardManager.getFirstItem() = + primaryClip?.takeIf { it.itemCount > 0 }?.getItemAt(0) fun getUri(clipboard: ClipboardManager?): Uri? { - return getFirstItem(clipboard)?.uri + return clipboard?.getFirstItem()?.uri + } + + 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)) + } + } + + fun getDescription(clipboard: ClipboardManager?): ClipDescription? { + return clipboard?.primaryClip?.description } @CheckResult fun getText(clipboard: ClipboardManager?): CharSequence? { - return getFirstItem(clipboard)?.text + return clipboard?.getFirstItem()?.text } @CheckResult fun getPlainText(clipboard: ClipboardManager?, context: Context): CharSequence? { - return getFirstItem(clipboard)?.coerceToText(context) + return clipboard?.getFirstItem()?.coerceToText(context) } } diff --git a/AnkiDroid/src/main/res/values/02-strings.xml b/AnkiDroid/src/main/res/values/02-strings.xml index 473e0fc16a9d..661c9475ea82 100644 --- a/AnkiDroid/src/main/res/values/02-strings.xml +++ b/AnkiDroid/src/main/res/values/02-strings.xml @@ -240,7 +240,9 @@ 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 image is too large, please insert the image manually + The video file is too large, please insert the video manually + The audio file is too large, please insert the audio 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..80522927a48b 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,48 @@ 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 hasMediaWithSVGMimeTypeTest() { + val clipDescription = ClipDescription("label", arrayOf("image/svg+xml")) + val clipData = ClipData(clipDescription, ClipData.Item("svg 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 640160781008..6f7dde922099 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,8 @@ androidxBrowser = "1.8.0" androidxConstraintlayout = "2.1.4" # https://developer.android.com/jetpack/androidx/releases/core androidxCoreKtx = "1.13.1" +# https://developer.android.com/jetpack/androidx/releases/draganddrop +androidxDragAndDrop = "1.0.0" # https://developer.android.com/jetpack/androidx/releases/exifinterface androidxExifinterface = "1.3.7" # https://developer.android.com/jetpack/androidx/releases/fragment @@ -106,6 +108,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" } diff --git a/testlib/src/main/AndroidManifest.xml b/testlib/src/main/AndroidManifest.xml index 568741e54f2a..0140c31c9d98 100644 --- a/testlib/src/main/AndroidManifest.xml +++ b/testlib/src/main/AndroidManifest.xml @@ -1,2 +1,15 @@ - \ No newline at end of file + + + + \ No newline at end of file