From 292d662d0b676edf21939b15c88dcf38fb3d83ba Mon Sep 17 00:00:00 2001 From: SanjaySargam Date: Wed, 24 Jul 2024 10:34:24 +0530 Subject: [PATCH] note-editor: drop files This commit ensures that we can drop files (photos, videos, and audio) in NoteEditor (FixedEditText). Fix: Handle minSdkVersion conflict with DropHelper and enable file drop in NoteEditor Added DropHelperCompat to handle the incompatibility issue with DropHelper and minSdkVersion 23. The manifest merger failed with the following error: > Manifest merger failed: uses-sdk:minSdkVersion 23 cannot be smaller than version 24 declared in library [androidx.draganddrop:draganddrop:1.0.0] /Users/davidallison/.gradle/cache> s/8.8/transforms/0a1688833d368c1b9b07d2054911030e/transformed/draganddrop-1.0.0/AndroidManifest.xml as the library might be using APIs not available in 23 > Suggestion: use a compatible library with a minSdk of at most 23, > or increase this project's minSdk version to at least 24, > or use tools:overrideLibrary="androidx.draganddrop" to force usage > (may lead to runtime failures) To resolve this, the DropHelperCompat class is used to conditionally configure the view for drag and drop operations only when the SDK version is 24 or higher. --- AnkiDroid/build.gradle | 1 + AnkiDroid/src/main/AndroidManifest.xml | 6 ++ .../main/java/com/ichi2/anki/FieldEditText.kt | 66 +++---------------- .../java/com/ichi2/anki/MediaRegistration.kt | 37 +++++++++-- .../main/java/com/ichi2/anki/NoteEditor.kt | 58 ++++++++++++++-- .../src/main/java/com/ichi2/compat/Compat.kt | 16 +++++ .../main/java/com/ichi2/compat/CompatV23.kt | 13 ++++ .../main/java/com/ichi2/compat/CompatV24.kt | 20 ++++++ .../java/com/ichi2/utils/ClipboardUtil.kt | 55 ++++++++++++---- AnkiDroid/src/main/res/values/02-strings.xml | 4 +- .../java/com/ichi2/utils/ClipboardUtilTest.kt | 64 ++++++++++++++++++ gradle/libs.versions.toml | 3 + testlib/src/main/AndroidManifest.xml | 15 ++++- 13 files changed, 275 insertions(+), 83 deletions(-) diff --git a/AnkiDroid/build.gradle b/AnkiDroid/build.gradle index 9d9009bab3..6f6199380d 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 414e159955..7819613e76 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 cea4773b0c..40d94e7fc3 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 f273722196..2ab46df5b9 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 a7b7499cc9..6c4d44e7cb 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 @@ -142,6 +148,8 @@ import com.ichi2.libanki.Notetypes.Companion.NOT_FOUND_NOTE_TYPE import com.ichi2.libanki.Utils 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 @@ -330,6 +338,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) @@ -1594,12 +1633,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 @@ -1837,8 +1887,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 6ac06a7aef..f0c0b783b3 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 264e257ba6..9112fbbf48 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 1ee05b2cc6..4edcf131c0 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 7e3e462b25..8b515799c1 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 473e0fc16a..661c9475ea 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 26c636c458..80522927a4 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 6401607810..6f7dde9220 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 568741e54f..0140c31c9d 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