diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-08-22 00:56:59 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-08-22 00:56:59 +0000 |
| commit | 00d873a3c5709200451c74b46860a67c1c1a52c7 (patch) | |
| tree | 87c090e78c55b34fba2e529a32c57644d002b989 /app/src/main/java/org/terst | |
| parent | 70c537fae9dc5e22bf8bb7bc226221b4e66b994d (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.kt | 66 |
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") } + } + } } } } |
