diff --git a/platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/EditorEditTextInputTests.kt b/platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/EditorEditTextInputTests.kt index de4ab9a8b..ba5e46d34 100644 --- a/platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/EditorEditTextInputTests.kt +++ b/platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/EditorEditTextInputTests.kt @@ -1,12 +1,18 @@ package io.element.android.wysiwyg +import android.content.ClipData +import android.content.ClipDescription +import android.content.ClipboardManager +import android.content.Context import android.graphics.Typeface +import android.net.Uri import android.text.Editable import android.text.style.BulletSpan import android.text.style.ReplacementSpan import android.text.style.StyleSpan import android.view.KeyEvent import android.view.View +import android.view.inputmethod.InputContentInfo import android.widget.EditText import android.widget.TextView import androidx.core.text.getSpans @@ -462,6 +468,79 @@ class EditorEditTextInputTests { confirmVerified(textWatcher) } + @Test + fun testPasteImage() { + val imageUri = Uri.parse("content://fakeImage") + val contentWatcher = spyk<(uri: Uri) -> Unit>({ }) + onView(withId(R.id.rich_text_edit_text)) + .perform(EditorActions.addContentWatcher(arrayOf("image/*"), contentWatcher)) + + scenarioRule.scenario.onActivity { activity -> + val editor = activity.findViewById(R.id.rich_text_edit_text) + val clipboardManager = + activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clpData = ClipData.newRawUri("image", imageUri) + clipboardManager.setPrimaryClip(clpData) + editor.onTextContextMenuItem(android.R.id.paste) + } + verify(exactly = 1) { + contentWatcher.invoke(match { it == imageUri }) + } + + confirmVerified(contentWatcher) + } + + @Test + fun testPastePlainText() { + val clipData = ClipData.newPlainText("text", ipsum) + val contentWatcher = spyk<(uri: Uri) -> Unit>({ }) + val textWatcher = spyk<(text: Editable?) -> Unit>({ }) + onView(withId(R.id.rich_text_edit_text)) + .perform(EditorActions.addTextWatcher(textWatcher)) + pasteFromClipboard(clipData, false) + + pasteFromClipboard(clipData, true) + + verify(exactly = 2) { + textWatcher.invoke(match { it.toString() == ipsum + ipsum }) + } + + confirmVerified(contentWatcher) + } + + @Test + fun testPasteHtlmText() { + val html = "$ipsum" + val clipData = ClipData.newHtmlText("html", ipsum, html) + val contentWatcher = spyk<(uri: Uri) -> Unit>({ }) + val textWatcher = spyk<(text: Editable?) -> Unit>({ }) + onView(withId(R.id.rich_text_edit_text)) + .perform(EditorActions.addTextWatcher(textWatcher)) + pasteFromClipboard(clipData, false) + + pasteFromClipboard(clipData, true) + + verify(exactly = 2) { + // In future when we support parsing/loading of pasted html into the model + // we can make more assertions on that the corrrect formating is applied + textWatcher.invoke(match { it.toString() == ipsum + ipsum }) + } + + confirmVerified(contentWatcher) + } + + + private fun pasteFromClipboard(clipData: ClipData, pasteAsPlainText: Boolean){ + scenarioRule.scenario.onActivity { activity -> + val editor = activity.findViewById(R.id.rich_text_edit_text) + val clipboardManager = + activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboardManager.setPrimaryClip(clipData) + val itemId = if (pasteAsPlainText) android.R.id.pasteAsPlainText else android.R.id.paste + editor.onTextContextMenuItem(itemId) + } + } + @Test fun getMarkdownTranslatesDomToMarkdown() { scenarioRule.scenario.onActivity { activity -> diff --git a/platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/test/utils/EditorActions.kt b/platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/test/utils/EditorActions.kt index d84d07ba5..f5aedeb6e 100644 --- a/platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/test/utils/EditorActions.kt +++ b/platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/test/utils/EditorActions.kt @@ -1,7 +1,9 @@ package io.element.android.wysiwyg.test.utils +import android.net.Uri import android.text.Editable import android.view.View +import androidx.core.view.ViewCompat import androidx.core.widget.addTextChangedListener import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction @@ -201,6 +203,27 @@ object Editor { } } + class AddContentWatcher( + private val contentTypes: Array, + private val contentWatcher: (Uri) -> Unit, + ) : ViewAction { + override fun getConstraints(): Matcher = isDisplayed() + + override fun getDescription(): String = "Add a content watcher" + + override fun perform(uiController: UiController?, view: View?) { + val editor = view as? EditorEditText ?: return + + ViewCompat.setOnReceiveContentListener( + editor, + contentTypes, + UriContentListener{ + contentWatcher(it) + } + ) + } + } + class TestCrash( private val errorCollector: RustErrorCollector? ) : ViewAction { @@ -232,7 +255,8 @@ object EditorActions { fun undo() = Editor.Undo fun redo() = Editor.Redo fun toggleFormat(format: InlineFormat) = Editor.ToggleFormat(format) - fun addTextWatcher(watcher : (Editable?) -> Unit) = Editor.AddTextWatcher(watcher) + fun addTextWatcher(watcher: (Editable?) -> Unit) = Editor.AddTextWatcher(watcher) + fun addContentWatcher(contentTypes: Array, watcher: (Uri) -> Unit) = Editor.AddContentWatcher(contentTypes, watcher) fun testCrash( errorCollector: RustErrorCollector? = null ) = Editor.TestCrash(errorCollector) diff --git a/platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/test/utils/UriContentListener.kt b/platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/test/utils/UriContentListener.kt new file mode 100644 index 000000000..3fe0532c9 --- /dev/null +++ b/platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/test/utils/UriContentListener.kt @@ -0,0 +1,29 @@ +package io.element.android.wysiwyg.test.utils + +import android.content.ClipData +import android.net.Uri +import android.view.View +import androidx.core.view.ContentInfoCompat +import androidx.core.view.OnReceiveContentListener + +class UriContentListener( + private val onContent: (uri: Uri) -> Unit +) : OnReceiveContentListener { + override fun onReceiveContent(view: View, payload: ContentInfoCompat): ContentInfoCompat? { + val split = payload.partition { item -> item.uri != null } + val uriContent = split.first + val remaining = split.second + + if (uriContent != null) { + val clip: ClipData = uriContent.clip + for (i in 0 until clip.itemCount) { + val uri = clip.getItemAt(i).uri + // ... app-specific logic to handle the URI ... + onContent(uri) + } + } + // Return anything that we didn't handle ourselves. This preserves the default platform + // behavior for text and anything else for which we are not implementing custom handling. + return remaining + } +} diff --git a/platforms/android/library/src/main/java/io/element/android/wysiwyg/EditorEditText.kt b/platforms/android/library/src/main/java/io/element/android/wysiwyg/EditorEditText.kt index 77d9b5c32..8149facc7 100644 --- a/platforms/android/library/src/main/java/io/element/android/wysiwyg/EditorEditText.kt +++ b/platforms/android/library/src/main/java/io/element/android/wysiwyg/EditorEditText.kt @@ -219,7 +219,9 @@ class EditorEditText : TextInputEditText { android.R.id.paste, android.R.id.pasteAsPlainText -> { val clipBoardManager = context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager - val copiedString = clipBoardManager.primaryClip?.getItemAt(0)?.text ?: return false + // Only special-case paste behaviour if it is text content, otherwise default to EditText implementation + // which calls ViewCompat.performReceiveContent and fires the expected listeners. + val copiedString = clipBoardManager.primaryClip?.getItemAt(0)?.text ?: return super.onTextContextMenuItem(id) val result = viewModel.processInput(EditorInputAction.ReplaceText(copiedString)) if (result != null) {