Skip to content

Commit

Permalink
note-editor: drop files
Browse files Browse the repository at this point in the history
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/caches/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.
  • Loading branch information
SanjaySargam committed Jul 24, 2024
1 parent 4cf695e commit 59559d4
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 96 deletions.
1 change: 1 addition & 0 deletions AnkiDroid/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions AnkiDroid/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto">

<!-- Requires minSdk 24, but we wrap this in compat -->
<uses-sdk tools:overrideLibrary="androidx.draganddrop"/>

<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
<uses-feature android:name="android.hardware.audio.output" android:required="false" />
Expand Down
77 changes: 14 additions & 63 deletions AnkiDroid/src/main/java/com/ichi2/anki/FieldEditText.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
}
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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 {
Expand Down
44 changes: 27 additions & 17 deletions AnkiDroid/src/main/java/com/ichi2/anki/MediaRegistration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String, String?>()
private val pastedMediaCache = HashMap<String, String?>()

/**
* 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)
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -129,42 +139,42 @@ 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

// 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
Expand Down
Loading

0 comments on commit 59559d4

Please sign in to comment.