Skip to content

Commit 2c64e4e

Browse files
committed
feat(compose): port crash-recovery screen to Compose + fix share-in review items
- RecoveryScreen: Compose replacement for legacy activity_recovery; shown by ComposeMainActivity when MyApp.recoveryMode is true. Restores crash-loop self-heal that went dead when the launcher moved off legacy MainActivity (recoveryMode check + launchSuccess reset were stranded there). - ComposeMainActivity: recovery branch in onCreate; onResume resets crash counter on a stable launch; share-in nav uses launchSingleTop + popUpTo (no double-push); multi-share validates all uris, not just the first.
1 parent 7f0ff69 commit 2c64e4e

2 files changed

Lines changed: 175 additions & 3 deletions

File tree

app/src/main/java/me/rosuh/easywatermark/ui/ComposeMainActivity.kt

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package me.rosuh.easywatermark.ui
22

33
import me.rosuh.cmonet.CMonet
4+
import android.content.ClipData
5+
import android.content.ClipboardManager
6+
import android.content.Context
47
import android.os.Build
58
import android.os.Bundle
69
import android.util.Log
@@ -46,6 +49,10 @@ import androidx.compose.ui.res.stringResource
4649
import androidx.compose.ui.window.DialogProperties
4750
import androidx.core.os.BuildCompat
4851
import androidx.lifecycle.compose.collectAsStateWithLifecycle
52+
import androidx.lifecycle.lifecycleScope
53+
import kotlinx.coroutines.delay
54+
import kotlinx.coroutines.launch
55+
import me.rosuh.easywatermark.MyApp
4956
import androidx.navigation.compose.NavHost
5057
import androidx.navigation.compose.composable
5158
import androidx.navigation.compose.dialog
@@ -97,8 +104,34 @@ class ComposeMainActivity : ComponentActivity() {
97104
?: emptyList()
98105
else -> emptyList()
99106
}
100-
if (uris.isNotEmpty() && FileUtils.isImage(contentResolver, uris.first())) {
101-
pendingShareUris = uris
107+
// Validate every shared uri, not just the first — a multi-share may mix in non-images.
108+
val images = uris.filter { FileUtils.isImage(contentResolver, it) }
109+
if (images.isNotEmpty()) {
110+
pendingShareUris = images
111+
}
112+
}
113+
114+
// A stable launch resets the crash counter. Ported from legacy MainActivity.onResume,
115+
// which became dead once ComposeMainActivity took over as launcher (ADR-0016).
116+
override fun onResume() {
117+
super.onResume()
118+
if (MyApp.recoveryMode) return
119+
lifecycleScope.launch {
120+
delay(1000)
121+
if (!isFinishing) (application as? MyApp)?.launchSuccess()
122+
}
123+
}
124+
125+
private fun crashStackTrace(): String =
126+
getSharedPreferences(MyApp.SP_NAME, MODE_PRIVATE).getString(MyApp.KEY_STACK_TRACE, "").orEmpty()
127+
128+
private fun copyCrashInfo(text: String) {
129+
try {
130+
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
131+
clipboard.setPrimaryClip(ClipData.newPlainText(text, text))
132+
Toast.makeText(this, R.string.copy_success, Toast.LENGTH_SHORT).show()
133+
} catch (e: Exception) {
134+
Toast.makeText(this, R.string.copy_failed, Toast.LENGTH_SHORT).show()
102135
}
103136
}
104137

@@ -107,6 +140,45 @@ class ComposeMainActivity : ComponentActivity() {
107140
super.onCreate(savedInstanceState)
108141
handleShareIntent(intent)
109142

143+
// Crash-recovery self-heal: MyApp.recoveryMode is computed in MyApp.onCreate.
144+
// Port of the legacy MainActivity activity_recovery branch (ADR-0016).
145+
if (MyApp.recoveryMode) {
146+
setContent {
147+
AppTheme {
148+
Surface(modifier = Modifier.fillMaxSize()) {
149+
RecoveryScreen(
150+
crashInfo = crashStackTrace(),
151+
onCopy = { copyCrashInfo(crashStackTrace()) },
152+
onSendEmail = {
153+
viewModel.extraCrashInfo(this@ComposeMainActivity, crashStackTrace())
154+
},
155+
onTelegram = { this@ComposeMainActivity.openLink("https://t.me/rosuh") },
156+
onStore = {
157+
this@ComposeMainActivity.openLink(
158+
Uri.parse("market://details?id=me.rosuh.easywatermark")
159+
) {
160+
Toast.makeText(
161+
this@ComposeMainActivity,
162+
R.string.store_not_found,
163+
Toast.LENGTH_SHORT
164+
).show()
165+
}
166+
},
167+
onCloseRecovery = {
168+
(application as MyApp).launchSuccess()
169+
Toast.makeText(
170+
this@ComposeMainActivity,
171+
R.string.recovery_mode_closed,
172+
Toast.LENGTH_SHORT
173+
).show()
174+
}
175+
)
176+
}
177+
}
178+
}
179+
return
180+
}
181+
110182
setContent {
111183
val windowSizeClass = calculateWindowSizeClass(this)
112184

@@ -149,7 +221,12 @@ class ComposeMainActivity : ComponentActivity() {
149221
LaunchedEffect(pendingShareUris) {
150222
pendingShareUris?.let { uris ->
151223
viewModel.updateImageList(uris)
152-
navController.navigate("EditorScreen")
224+
// launchSingleTop + popUpTo: a share received while already in the
225+
// editor (onNewIntent) must not stack a second EditorScreen.
226+
navController.navigate("EditorScreen") {
227+
launchSingleTop = true
228+
popUpTo("LaunchScreen")
229+
}
153230
pendingShareUris = null
154231
}
155232
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package me.rosuh.easywatermark.ui
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Row
6+
import androidx.compose.foundation.layout.Spacer
7+
import androidx.compose.foundation.layout.fillMaxSize
8+
import androidx.compose.foundation.layout.fillMaxWidth
9+
import androidx.compose.foundation.layout.height
10+
import androidx.compose.foundation.layout.padding
11+
import androidx.compose.foundation.layout.safeDrawingPadding
12+
import androidx.compose.foundation.rememberScrollState
13+
import androidx.compose.foundation.verticalScroll
14+
import androidx.compose.material3.Button
15+
import androidx.compose.material3.MaterialTheme
16+
import androidx.compose.material3.Surface
17+
import androidx.compose.material3.Text
18+
import androidx.compose.material3.TextButton
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.ui.Alignment
21+
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.res.stringResource
23+
import androidx.compose.ui.text.style.TextAlign
24+
import androidx.compose.ui.unit.dp
25+
import me.rosuh.easywatermark.R
26+
27+
/**
28+
* Compose replacement for the legacy `activity_recovery.xml` crash-recovery screen
29+
* (View→Compose migration, ADR-0016). Shown by [ComposeMainActivity] when
30+
* `MyApp.recoveryMode` is true — the crash-loop self-heal surface. Pure UI + callbacks;
31+
* the host wires clipboard, email, links, and recovery-mode reset.
32+
*/
33+
@Composable
34+
fun RecoveryScreen(
35+
crashInfo: String,
36+
onCopy: () -> Unit,
37+
onSendEmail: () -> Unit,
38+
onTelegram: () -> Unit,
39+
onStore: () -> Unit,
40+
onCloseRecovery: () -> Unit,
41+
modifier: Modifier = Modifier,
42+
) {
43+
Column(
44+
modifier = modifier
45+
.fillMaxSize()
46+
.safeDrawingPadding()
47+
.padding(16.dp),
48+
horizontalAlignment = Alignment.CenterHorizontally
49+
) {
50+
Text(
51+
text = stringResource(R.string.recovery_title),
52+
style = MaterialTheme.typography.titleLarge,
53+
color = MaterialTheme.colorScheme.onSurface,
54+
modifier = Modifier.padding(top = 16.dp)
55+
)
56+
Text(
57+
text = stringResource(R.string.recovery_mode_tips),
58+
style = MaterialTheme.typography.titleMedium,
59+
color = MaterialTheme.colorScheme.onSurface,
60+
textAlign = TextAlign.Center,
61+
modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp)
62+
)
63+
Surface(
64+
modifier = Modifier
65+
.fillMaxWidth()
66+
.weight(1f),
67+
color = MaterialTheme.colorScheme.errorContainer,
68+
shape = MaterialTheme.shapes.medium
69+
) {
70+
Text(
71+
text = crashInfo,
72+
style = MaterialTheme.typography.bodySmall,
73+
color = MaterialTheme.colorScheme.onErrorContainer,
74+
modifier = Modifier
75+
.verticalScroll(rememberScrollState())
76+
.padding(16.dp)
77+
)
78+
}
79+
Spacer(Modifier.height(12.dp))
80+
Button(onClick = onCopy) {
81+
Text(stringResource(R.string.copy))
82+
}
83+
Row(
84+
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
85+
horizontalArrangement = Arrangement.SpaceEvenly
86+
) {
87+
TextButton(onClick = onSendEmail) { Text("Send email") }
88+
TextButton(onClick = onTelegram) { Text("Send Telegram") }
89+
TextButton(onClick = onStore) { Text("Jump to Store") }
90+
}
91+
TextButton(onClick = onCloseRecovery) {
92+
Text(stringResource(R.string.turn_off_recovery_mode))
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)