Files
SyncFlow/app/src/main/kotlin/com/syncflow/MainActivity.kt
T
amir cff4233de6 Initial commit — SyncFlow Android file sync app
Supports WebDAV, SFTP, SFTPGo, Nextcloud, ownCloud, Google Drive,
Dropbox, and OneDrive. Credentials encrypted with Android Keystore.
Biometric app-lock, conflict resolution, and auto-sync via WorkManager.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 20:21:20 +00:00

113 lines
4.0 KiB
Kotlin

package com.syncflow
import android.os.Build
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt
import androidx.compose.foundation.background
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.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.rememberNavController
import com.syncflow.data.preferences.AppPreferences
import com.syncflow.ui.navigation.SyncFlowNavGraph
import com.syncflow.ui.theme.SyncFlowTheme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var appPreferences: AppPreferences
private var isLocked by mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
SyncFlowTheme {
Surface(modifier = Modifier.fillMaxSize()) {
SyncFlowNavGraph(rememberNavController())
}
if (isLocked) {
LockOverlay()
LaunchedEffect(Unit) {
showBiometricPrompt(onSuccess = { isLocked = false })
}
}
}
}
}
override fun onStop() {
super.onStop()
if (isChangingConfigurations) return
lifecycleScope.launch {
if (appPreferences.biometricLockEnabled.first() && canAuthenticate()) {
isLocked = true
}
}
}
private fun canAuthenticate(): Boolean {
val authenticators = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
else
BIOMETRIC_STRONG
return BiometricManager.from(this).canAuthenticate(authenticators) ==
BiometricManager.BIOMETRIC_SUCCESS
}
private fun showBiometricPrompt(onSuccess: () -> Unit) {
val executor = ContextCompat.getMainExecutor(this)
val prompt = BiometricPrompt(this, executor, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
onSuccess()
}
})
val promptInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock SyncFlow")
.setSubtitle("Confirm your identity to continue")
.setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
.build()
} else {
BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock SyncFlow")
.setSubtitle("Confirm your identity to continue")
.setNegativeButtonText("Cancel")
.build()
}
prompt.authenticate(promptInfo)
}
}
@Composable
private fun LockOverlay() {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
}