Files
Amber/app/src/main/java/com/greenart7c3/nostrsigner/MainActivity.kt
T
Claude 91aadf89ae Use ImmutableList for request list parameters through the composable chain
Plain List<T> has runtime-determined stability in Compose, preventing
compile-time skip optimizations. Switching to ImmutableList<T> (which
is @Immutable-annotated) lets the compiler guarantee that composables
receiving unchanged list references can be safely skipped.

Also fix redundant smart-cast in BunkerMultiEventHomeScreen.

https://claude.ai/code/session_01PRSKjmFms1Tj1QJ5FhfqZm
2026-03-20 12:46:57 +00:00

201 lines
8.9 KiB
Kotlin

package com.greenart7c3.nostrsigner
import android.content.Intent
import android.net.ConnectivityManager
import android.os.Bundle
import android.util.Log
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.rememberNavController
import com.greenart7c3.nostrsigner.okhttp.HttpClientManager
import com.greenart7c3.nostrsigner.service.BunkerRequestUtils
import com.greenart7c3.nostrsigner.ui.AccountScreen
import com.greenart7c3.nostrsigner.ui.AccountStateViewModel
import com.greenart7c3.nostrsigner.ui.CenterCircularProgressIndicator
import com.greenart7c3.nostrsigner.ui.components.BiometricAuthScreen
import com.greenart7c3.nostrsigner.ui.navigation.Route
import com.greenart7c3.nostrsigner.ui.theme.NostrSignerTheme
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.utils.Hex
import java.time.Duration
import java.time.Instant
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
fun Intent.isLaunchFromHistory(): Boolean = this.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY == Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
class MainActivity : AppCompatActivity() {
lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
Log.d(Amber.TAG, "onCreate MainActivity")
Amber.isAppInForeground = true
enableEdgeToEdge()
super.onCreate(savedInstanceState)
Amber.instance.setMainActivity(this)
mainViewModel = MainViewModel(applicationContext)
setContent {
val isStartingApp = Amber.instance.isStartingAppState.collectAsStateWithLifecycle()
if (isStartingApp.value) {
NostrSignerTheme {
CenterCircularProgressIndicator(Modifier, "Starting application...")
}
} else {
HttpClientManager.setDefaultUserAgent("Amber/${BuildConfig.VERSION_NAME}")
if (intent?.isLaunchFromHistory() == true) {
Log.d(Amber.TAG, "Cleared intent history")
intent = Intent()
}
val packageName = callingPackage
val appName =
if (packageName != null) {
try {
val info = applicationContext.packageManager.getApplicationInfo(packageName, 0)
applicationContext.packageManager.getApplicationLabel(info).toString()
} catch (_: Exception) {
null
}
} else {
null
}
NostrSignerTheme {
CompositionLocalProvider(
LocalDensity provides Density(
LocalDensity.current.density,
1f,
),
) {
val navController = rememberNavController()
mainViewModel.navController = navController
val context = LocalContext.current
var isAuthenticated by remember { mutableStateOf(false) }
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
) {
BiometricAuthScreen(
onAuth = {
isAuthenticated = it
},
)
if (!isAuthenticated) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
} else {
val bunkerRequests = BunkerRequestUtils.state.collectAsStateWithLifecycle(persistentListOf())
val npub = remember { mainViewModel.getAccount(intent?.getStringExtra("current_user")) }
val accountStateViewModel: AccountStateViewModel =
viewModel {
AccountStateViewModel(npub)
}
LaunchedEffect(Unit, bunkerRequests.value) {
launch(Dispatchers.IO) {
val localNpub = mainViewModel.getAccount(intent?.getStringExtra("current_user"))
val currentAccount = LocalPreferences.currentAccount(context)
if (currentAccount != null && localNpub != null && currentAccount != localNpub && localNpub.isNotBlank()) {
if (localNpub.startsWith("npub")) {
Log.d(Amber.TAG, "Switching account to $localNpub")
if (LocalPreferences.containsAccount(context, localNpub)) {
accountStateViewModel.switchUser(localNpub, Route.IncomingRequest.route)
}
} else {
val localNpub2 = Hex.decode(localNpub).toNpub()
Log.d(Amber.TAG, "Switching account to $localNpub2")
if (LocalPreferences.containsAccount(context, localNpub2)) {
accountStateViewModel.switchUser(localNpub2, Route.IncomingRequest.route)
}
}
}
}
}
AccountScreen(
accountStateViewModel = accountStateViewModel,
intent = intent,
packageName = packageName,
appName = appName,
mainViewModel = mainViewModel,
bunkerRequests = bunkerRequests.value,
navController = navController,
)
}
}
}
}
}
}
}
override fun onPause() {
Log.d(Amber.TAG, "onPause MainActivity")
Amber.isAppInForeground = false
super.onPause()
}
override fun onResume() {
Log.d(Amber.TAG, "onResume MainActivity")
Amber.isAppInForeground = true
Amber.instance.setMainActivity(this)
Amber.instance.startServiceFromUi()
mainViewModel.showBunkerRequests()
if (!BuildFlavorChecker.isOfflineFlavor()) {
val connectivityManager =
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)?.let {
Amber.instance.updateNetworkCapabilities(it)
}
}
super.onResume()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
mainViewModel.onNewIntent(intent, callingPackage)
}
override fun onDestroy() {
mainViewModel.clear()
super.onDestroy()
}
}
fun minutesBetween(startMillis: Long, endMillis: Long): Long {
val start = Instant.ofEpochMilli(startMillis)
val end = Instant.ofEpochMilli(endMillis)
val duration = Duration.between(start, end)
return duration.toMinutes()
}