Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions android/src/main/java/ShareIntentPayload.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package plugin.vnidrop.share

internal data class ShareIntentPayload(
val title: String?,
val body: String?,
) {
companion object {
fun from(options: ShareOptions): ShareIntentPayload {
val text = options.text.nonEmptyOrNull()
val title = options.title.nonEmptyOrNull()
val url = options.url.nonEmptyOrNull()

return ShareIntentPayload(
title = title ?: text,
body = url ?: text,
)
}

private fun String?.nonEmptyOrNull(): String? = this?.takeIf { it.isNotEmpty() }
}
}
63 changes: 30 additions & 33 deletions android/src/main/java/SharePlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import android.os.Looper
import android.util.Base64
import java.io.File
import java.io.FileOutputStream
import androidx.activity.result.ActivityResult
import androidx.core.content.FileProvider
import app.tauri.annotation.ActivityCallback
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.TauriPlugin
Expand Down Expand Up @@ -37,9 +39,8 @@ class ShareOptions {
@TauriPlugin
class SharePlugin(private val activity: Activity): Plugin(activity) {
private var pendingShareInvoke: Invoke? = null
private var shareInProgress = false
private var awaitingShareResume = false
private var pendingCleanupFiles: List<File> = emptyList()
private val shareSession = ShareSessionState()
private val cleanupHandler = Handler(Looper.getMainLooper())

companion object {
Expand All @@ -57,7 +58,7 @@ class SharePlugin(private val activity: Activity): Plugin(activity) {

@Command
fun share(invoke: Invoke) {
if (shareInProgress) {
if (shareSession.isInProgress) {
invoke.reject("Share already in progress.")
return
}
Expand Down Expand Up @@ -111,22 +112,22 @@ class SharePlugin(private val activity: Activity): Plugin(activity) {

shareIntent.type = determinedMimeType

val combinedText = combineTextAndUrl(args.text, args.url)
if (combinedText != null) {
shareIntent.putExtra(Intent.EXTRA_TEXT, combinedText)
val payload = ShareIntentPayload.from(args)
payload.body?.let {
shareIntent.putExtra(Intent.EXTRA_TEXT, it)
}
if (args.title != null) {
shareIntent.putExtra(Intent.EXTRA_TITLE, args.title)
payload.title?.let {
shareIntent.putExtra(Intent.EXTRA_TITLE, it)
shareIntent.putExtra(Intent.EXTRA_SUBJECT, it)
}

shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val chooser = Intent.createChooser(shareIntent, args.title)
val chooser = Intent.createChooser(shareIntent, payload.title)

pendingShareInvoke = invoke
shareInProgress = true
awaitingShareResume = false
pendingCleanupFiles = filesForShare
activity.startActivity(chooser)
shareSession.start()
startActivityForResult(invoke, chooser, "shareResult")
} catch (e: Exception) {
cleanupFiles(filesForShare)
resetPendingShare()
Expand All @@ -136,19 +137,20 @@ class SharePlugin(private val activity: Activity): Plugin(activity) {

override fun onPause() {
super.onPause()
if (shareInProgress) {
awaitingShareResume = true
}
shareSession.markPaused()
}

override fun onResume() {
super.onResume()
if (shareInProgress && awaitingShareResume) {
val invoke = pendingShareInvoke
val filesToClean = pendingCleanupFiles
resetPendingShare()
scheduleCleanup(filesToClean)
invoke?.resolve()
if (shareSession.completeFromResume()) {
resolvePendingShare()
}
}

@ActivityCallback
fun shareResult(invoke: Invoke, result: ActivityResult) {
if (shareSession.completeFromActivityResult()) {
resolvePendingShare(invoke)
}
}
Comment on lines +150 to 155

Expand Down Expand Up @@ -194,23 +196,18 @@ class SharePlugin(private val activity: Activity): Plugin(activity) {
return clipData
}

private fun combineTextAndUrl(text: String?, url: String?): String? {
val hasText = !text.isNullOrEmpty()
val hasUrl = !url.isNullOrEmpty()

return when {
hasText && hasUrl -> "$text\n$url"
hasText -> text
hasUrl -> url
else -> null
}
private fun resolvePendingShare(invoke: Invoke? = pendingShareInvoke) {
val filesToClean = pendingCleanupFiles
pendingShareInvoke = null
pendingCleanupFiles = emptyList()
scheduleCleanup(filesToClean)
invoke?.resolve()
}

private fun resetPendingShare() {
pendingShareInvoke = null
shareInProgress = false
awaitingShareResume = false
pendingCleanupFiles = emptyList()
shareSession.reset()
}

private fun scheduleCleanup(files: List<File>) {
Expand Down
38 changes: 38 additions & 0 deletions android/src/main/java/ShareSessionState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package plugin.vnidrop.share

internal class ShareSessionState {
var isInProgress: Boolean = false
private set

private var awaitingResume: Boolean = false

fun start() {
check(!isInProgress) { "Share session already in progress." }
isInProgress = true
awaitingResume = false
}

fun markPaused() {
if (isInProgress) {
awaitingResume = true
}
}

fun completeFromActivityResult(): Boolean = completeIf(isInProgress)

fun completeFromResume(): Boolean = completeIf(isInProgress && awaitingResume)

fun reset() {
isInProgress = false
awaitingResume = false
}

private fun completeIf(shouldComplete: Boolean): Boolean {
if (!shouldComplete) {
return false
}

reset()
return true
}
}
76 changes: 76 additions & 0 deletions android/src/test/java/ShareIntentPayloadUnitTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package plugin.vnidrop.share

import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test

class ShareIntentPayloadUnitTest {
@Test
fun usesUrlAsBodyAndTextAsFallbackTitle() {
val payload = ShareIntentPayload.from(
options(
text = "Read this article",
url = "https://example.com/article",
)
)

assertEquals("Read this article", payload.title)
assertEquals("https://example.com/article", payload.body)
}

@Test
fun explicitTitleTakesPrecedenceOverText() {
val payload = ShareIntentPayload.from(
options(
text = "Description",
title = "Article title",
url = "https://example.com/article",
)
)

assertEquals("Article title", payload.title)
assertEquals("https://example.com/article", payload.body)
}

@Test
fun textOnlyPayloadUsesTextAsTitleAndBody() {
val payload = ShareIntentPayload.from(options(text = "Hello"))

assertEquals("Hello", payload.title)
assertEquals("Hello", payload.body)
}

@Test
fun emptyValuesDoNotHideShareableText() {
val payload = ShareIntentPayload.from(
options(
text = "Hello",
title = "",
url = "",
)
)

assertEquals("Hello", payload.title)
assertEquals("Hello", payload.body)
}

@Test
fun emptyPayloadHasNoTitleOrBody() {
val payload = ShareIntentPayload.from(options())

assertNull(payload.title)
assertNull(payload.body)
}

private fun options(
text: String? = null,
title: String? = null,
url: String? = null,
): ShareOptions {
return ShareOptions().apply {
this.text = text
this.title = title
this.url = url
}
}
}
43 changes: 43 additions & 0 deletions android/src/test/java/ShareSessionStateUnitTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package plugin.vnidrop.share

import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class ShareSessionStateUnitTest {
@Test
fun activityResultCompletesSessionAndAllowsAnotherShare() {
val state = ShareSessionState()

state.start()
assertTrue(state.isInProgress)
assertTrue(state.completeFromActivityResult())
assertFalse(state.isInProgress)

state.start()
assertTrue(state.isInProgress)
}

@Test
fun resumeRemainsAFallbackAfterPause() {
val state = ShareSessionState()

state.start()
assertFalse(state.completeFromResume())

state.markPaused()
assertTrue(state.completeFromResume())
assertFalse(state.isInProgress)
}

@Test
fun completionOnlyHappensOnce() {
val state = ShareSessionState()

state.start()
state.markPaused()
assertTrue(state.completeFromActivityResult())
assertFalse(state.completeFromResume())
assertFalse(state.completeFromActivityResult())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package app.tauri.annotation

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ActivityCallback
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package app.tauri.plugin

import android.app.Activity
import android.content.Intent

open class Plugin(activity: Activity) {
@Suppress("UNUSED_VARIABLE")
Expand All @@ -9,4 +10,13 @@ open class Plugin(activity: Activity) {
open fun onPause() {}

open fun onResume() {}

fun startActivityForResult(invoke: Invoke, intent: Intent, callback: String) {
@Suppress("UNUSED_VARIABLE")
val ignoredInvoke = invoke
@Suppress("UNUSED_VARIABLE")
val ignoredIntent = intent
@Suppress("UNUSED_VARIABLE")
val ignoredCallback = callback
}
}
Loading