Seed and synchronously read hadNoDictionaries before setContent so Settings shows from the first frame until the startup scan confirms.
224 lines
8.8 KiB
Kotlin
224 lines
8.8 KiB
Kotlin
package com.example.research
|
|
|
|
import android.Manifest
|
|
import android.content.Intent
|
|
import android.content.pm.PackageManager
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import androidx.activity.ComponentActivity
|
|
import androidx.activity.compose.setContent
|
|
import androidx.activity.enableEdgeToEdge
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
import androidx.activity.viewModels
|
|
import androidx.compose.foundation.isSystemInDarkTheme
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.Surface
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.core.content.ContextCompat
|
|
import androidx.core.content.edit
|
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
import androidx.lifecycle.createSavedStateHandle
|
|
import androidx.lifecycle.viewmodel.initializer
|
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
|
import com.example.research.common.ui.AppLocaleProvider
|
|
import com.example.research.common.ui.BackgroundTextMeasurementProvider
|
|
import com.example.research.common.ui.theme.ReSearchTheme
|
|
import com.example.research.core.domain.model.AppTheme
|
|
import com.example.research.data.local.preferences.SectionExpansionStates
|
|
import com.example.research.feature.search.SearchAction
|
|
import com.example.research.feature.search.SearchViewModel
|
|
import com.example.research.ui.navigation.AppNavigation
|
|
import com.example.research.ui.settings.SettingsViewModel
|
|
import kotlinx.coroutines.flow.first
|
|
import kotlinx.coroutines.runBlocking
|
|
|
|
class MainActivity : ComponentActivity() {
|
|
companion object {
|
|
private const val PREF_NOTIFICATION_PERMISSION_REQUESTED = "notification_permission_requested"
|
|
}
|
|
|
|
private val notificationPermissionLauncher = registerForActivityResult(
|
|
ActivityResultContracts.RequestPermission()
|
|
) { /* Permission result handled silently */ }
|
|
|
|
private val searchViewModel: SearchViewModel by viewModels {
|
|
val app = application as ReSearchApplication
|
|
viewModelFactory {
|
|
initializer {
|
|
SearchViewModel(application, app.localDictionaryRepository, createSavedStateHandle())
|
|
}
|
|
}
|
|
}
|
|
|
|
private var initialSectionExpansion: SectionExpansionStates? = null
|
|
|
|
private val settingsViewModel: SettingsViewModel by viewModels {
|
|
val app = application as ReSearchApplication
|
|
val versionName = try {
|
|
packageManager.getPackageInfo(packageName, 0).versionName ?: "1.0"
|
|
} catch (_: Exception) {
|
|
"1.0"
|
|
}
|
|
viewModelFactory {
|
|
initializer {
|
|
SettingsViewModel(
|
|
application,
|
|
app.preferencesManager,
|
|
app.downloadDictionaryRepository,
|
|
app.downloadManager,
|
|
app.localDictionaryRepository,
|
|
app.dictionaryImportManager,
|
|
app.dictionaryProgressStateHolder,
|
|
versionName,
|
|
initialSectionExpansion ?: SectionExpansionStates(
|
|
isThemeExpanded = false,
|
|
isLanguageExpanded = false,
|
|
isDictionariesExpanded = true,
|
|
),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
enableEdgeToEdge()
|
|
super.onCreate(savedInstanceState)
|
|
requestNotificationPermissionIfNeeded()
|
|
|
|
// Load theme and language synchronously before setContent to prevent flashing
|
|
val app = application as ReSearchApplication
|
|
val initialTheme = loadInitialTheme(app)
|
|
val initialLanguage = loadInitialLanguage(app)
|
|
val initialHadNoDictionaries = loadInitialHadNoDictionaries(app)
|
|
initialSectionExpansion = loadInitialSectionExpansion(app)
|
|
|
|
handleIntent(intent)
|
|
setContent {
|
|
val settingsState by settingsViewModel.uiState.collectAsStateWithLifecycle()
|
|
// Use loaded values initially, then switch to state values when ready
|
|
val theme = settingsState.theme.takeIf { it != AppTheme.SYSTEM } ?: initialTheme
|
|
val language = settingsState.language.takeIf { it != "system" } ?: initialLanguage
|
|
val darkTheme = when (theme) {
|
|
AppTheme.LIGHT -> false
|
|
AppTheme.DARK -> true
|
|
AppTheme.SYSTEM -> isSystemInDarkTheme()
|
|
}
|
|
AppLocaleProvider(language = language) {
|
|
ReSearchTheme(darkTheme = darkTheme) {
|
|
BackgroundTextMeasurementProvider {
|
|
Surface(
|
|
modifier = Modifier.fillMaxSize(),
|
|
color = MaterialTheme.colorScheme.background
|
|
) {
|
|
AppNavigation(
|
|
searchViewModel = searchViewModel,
|
|
settingsViewModel = settingsViewModel,
|
|
seedNoDictionaries = initialHadNoDictionaries
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun requestNotificationPermissionIfNeeded() {
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
|
|
val permission = Manifest.permission.POST_NOTIFICATIONS
|
|
val isGranted = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
|
|
val prefs = getPreferences(MODE_PRIVATE)
|
|
val wasRequested = prefs.getBoolean(PREF_NOTIFICATION_PERMISSION_REQUESTED, false)
|
|
if (!isGranted && !wasRequested) {
|
|
prefs.edit {
|
|
putBoolean(PREF_NOTIFICATION_PERMISSION_REQUESTED, true)
|
|
}
|
|
notificationPermissionLauncher.launch(permission)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load initial theme synchronously from DataStore to prevent theme flashing.
|
|
* This is called before setContent to ensure the correct theme is applied immediately.
|
|
* Uses runBlocking to synchronously read from DataStore.
|
|
*/
|
|
private fun loadInitialTheme(app: ReSearchApplication): AppTheme {
|
|
return try {
|
|
runBlocking {
|
|
val themeString = app.preferencesManager.themeMode.first()
|
|
try {
|
|
AppTheme.valueOf(themeString)
|
|
} catch (_: Exception) {
|
|
android.util.Log.w("MainActivity", "Invalid theme value: $themeString, using SYSTEM")
|
|
AppTheme.SYSTEM
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
android.util.Log.e("MainActivity", "Failed to load theme preference: ${e.message}")
|
|
AppTheme.SYSTEM
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load initial language synchronously from DataStore to prevent language flashing.
|
|
* This is called before setContent to ensure the correct language is applied immediately.
|
|
* Uses runBlocking to synchronously read from DataStore.
|
|
*/
|
|
private fun loadInitialLanguage(app: ReSearchApplication): String {
|
|
return try {
|
|
runBlocking {
|
|
app.preferencesManager.language.first()
|
|
}
|
|
} catch (e: Exception) {
|
|
android.util.Log.e("MainActivity", "Failed to load language preference: ${e.message}")
|
|
"system"
|
|
}
|
|
}
|
|
|
|
private fun loadInitialHadNoDictionaries(app: ReSearchApplication): Boolean {
|
|
return try {
|
|
runBlocking {
|
|
app.preferencesManager.hadNoDictionaries.first()
|
|
}
|
|
} catch (e: Exception) {
|
|
true
|
|
}
|
|
}
|
|
|
|
private fun loadInitialSectionExpansion(app: ReSearchApplication): SectionExpansionStates {
|
|
return try {
|
|
runBlocking {
|
|
app.preferencesManager.sectionExpansionStates()
|
|
}
|
|
} catch (e: Exception) {
|
|
android.util.Log.e("MainActivity", "Failed to load section expansion states: ${e.message}")
|
|
SectionExpansionStates(
|
|
isThemeExpanded = false,
|
|
isLanguageExpanded = false,
|
|
isDictionariesExpanded = true,
|
|
)
|
|
}
|
|
}
|
|
|
|
override fun onNewIntent(intent: Intent) {
|
|
super.onNewIntent(intent)
|
|
handleIntent(intent)
|
|
}
|
|
|
|
override fun onUserLeaveHint() {
|
|
super.onUserLeaveHint()
|
|
// Debug hook for user leaving the app
|
|
}
|
|
|
|
private fun handleIntent(intent: Intent) {
|
|
if (intent.action == Intent.ACTION_PROCESS_TEXT) {
|
|
val text = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT)
|
|
?: intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT_READONLY)
|
|
text?.toString()?.let {
|
|
searchViewModel.onAction(SearchAction.HandleExternalSearch(it))
|
|
}
|
|
}
|
|
}
|
|
}
|