summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-08-22 00:56:59 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-08-22 00:56:59 +0000
commit00d873a3c5709200451c74b46860a67c1c1a52c7 (patch)
tree87c090e78c55b34fba2e529a32c57644d002b989 /app/src/main/java/org/terst
parent70c537fae9dc5e22bf8bb7bc226221b4e66b994d (diff)
Require explicit tap before ShareReceiverActivity saves anything
This activity is exported (any installed app can target it directly with an ACTION_SEND intent, not just via the system share sheet). Starting extraction/Stash-save immediately on launch meant any other app could silently make this one fetch+JS-render an arbitrary URL and archive it to the user's own Stash server with zero user interaction. A tap is now the only thing standing between "another app sent an intent" and "real network work happens." Also switches to the shared saveArticleToStash helper while touching this same code path.
Diffstat (limited to 'app/src/main/java/org/terst')
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/ShareReceiverActivity.kt66
1 files changed, 39 insertions, 27 deletions
diff --git a/app/src/main/java/org/terst/feedreader/ui/ShareReceiverActivity.kt b/app/src/main/java/org/terst/feedreader/ui/ShareReceiverActivity.kt
index 1a383ae..70d45f0 100644
--- a/app/src/main/java/org/terst/feedreader/ui/ShareReceiverActivity.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/ShareReceiverActivity.kt
@@ -22,11 +22,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import org.terst.feedreader.data.AppDatabase
-import org.terst.feedreader.data.Net
-import org.terst.feedreader.data.Settings
+import org.terst.feedreader.data.StashSaveOutcome
import org.terst.feedreader.data.ensureStandaloneArticle
-import org.terst.feedreader.stash.StashClient
-import org.terst.feedreader.stash.StashSaveResult
+import org.terst.feedreader.data.saveArticleToStash
private val URL_REGEX = Regex("""https?://\S+""")
@@ -54,29 +52,29 @@ class ShareReceiverActivity : ComponentActivity() {
@Composable
private fun ShareReceiverContent(url: String?, onDone: () -> Unit) {
val context = LocalContext.current
- var status by remember { mutableStateOf(if (url == null) "No link found in shared content" else "Saving…") }
+ var status by remember { mutableStateOf(if (url == null) "No link found in shared content" else null) }
var done by remember { mutableStateOf(url == null) }
+ // Requires an explicit tap before touching the network -- this activity is exported
+ // (any installed app can target it directly with an ACTION_SEND intent, not just via
+ // the system share sheet), and starting extraction/Stash-save immediately on launch
+ // meant ANY other app could silently make this one fetch+JS-render an arbitrary URL
+ // and archive it to the user's own Stash server with zero user interaction. A tap
+ // here is the only thing standing between "another app sent an intent" and "this app
+ // did real network work," so it can't be skipped just because url is non-null.
+ var confirmed by remember { mutableStateOf(false) }
- LaunchedEffect(url) {
- if (url == null) return@LaunchedEffect
+ LaunchedEffect(url, confirmed) {
+ if (url == null || !confirmed) return@LaunchedEffect
+ status = "Saving…"
val db = AppDatabase.get(context)
val entity = ensureStandaloneArticle(context, db, url)
if (!entity.isSaved) db.articleDao().setSaved(entity.id, true)
- val settings = Settings(context)
- val client = StashClient(Net.client, settings.stashBaseUrlNow(), settings.stashApiKeyNow())
- status = when (val result = client.saveArticle(url, entity.title)) {
- is StashSaveResult.Saved -> {
- db.articleDao().setStashArtifactId(entity.id, result.artifact.id)
- "Saved for offline reading and archived to Stash"
- }
- is StashSaveResult.AlreadySaved -> {
- db.articleDao().setStashArtifactId(entity.id, result.existingId)
- "Saved for offline reading (already in Stash)"
- }
- is StashSaveResult.Failed ->
- "Saved for offline reading. Stash archive failed: ${result.message}"
+ status = when (val outcome = saveArticleToStash(context, db, entity.id, url, entity.title)) {
+ is StashSaveOutcome.Saved -> "Saved for offline reading and archived to Stash"
+ is StashSaveOutcome.AlreadySaved -> "Saved for offline reading (already in Stash)"
+ is StashSaveOutcome.Failed -> "Saved for offline reading. Stash archive failed: ${outcome.reason}"
}
done = true
}
@@ -86,13 +84,27 @@ private fun ShareReceiverContent(url: String?, onDone: () -> Unit) {
Column(modifier = Modifier.padding(20.dp)) {
Text("Feeds", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
- Text(status)
- if (!done) {
- Spacer(modifier = Modifier.height(8.dp))
- CircularProgressIndicator()
- } else {
- Spacer(modifier = Modifier.height(8.dp))
- Button(onClick = onDone) { Text("Done") }
+ when {
+ url == null -> {
+ Text(status ?: "No link found in shared content")
+ Spacer(modifier = Modifier.height(8.dp))
+ Button(onClick = onDone) { Text("Done") }
+ }
+ !confirmed -> {
+ Text(url)
+ Spacer(modifier = Modifier.height(8.dp))
+ Button(onClick = { confirmed = true }) { Text("Save") }
+ }
+ else -> {
+ Text(status ?: "Saving…")
+ if (!done) {
+ Spacer(modifier = Modifier.height(8.dp))
+ CircularProgressIndicator()
+ } else {
+ Spacer(modifier = Modifier.height(8.dp))
+ Button(onClick = onDone) { Text("Done") }
+ }
+ }
}
}
}