package com.shaarit import android.net.Uri import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.fragment.app.FragmentActivity import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import com.shaarit.core.storage.BiometricAuthManager import com.shaarit.core.storage.SecurityPreferences import com.shaarit.presentation.auth.LockScreen import com.shaarit.domain.model.AudioContentType import com.shaarit.domain.model.AudioMedia import com.shaarit.presentation.audio.AudioHandler import com.shaarit.presentation.audio.FullPlayerSheet import com.shaarit.presentation.audio.MiniPlayerBar import com.shaarit.presentation.nav.AppNavGraph import com.shaarit.ui.theme.ShaarItTheme import com.shaarit.ui.theme.ThemePreferences import dagger.hilt.android.AndroidEntryPoint import java.io.BufferedReader import java.io.InputStreamReader import javax.inject.Inject @AndroidEntryPoint class MainActivity : FragmentActivity() { @Inject lateinit var themePreferences: ThemePreferences @Inject lateinit var tokenManager: com.shaarit.core.storage.TokenManager @Inject lateinit var securityPreferences: SecurityPreferences @Inject lateinit var biometricAuthManager: BiometricAuthManager @Inject lateinit var audioHandler: AudioHandler // Start as authenticated — lock only triggers after app goes to background private var isAuthenticated by mutableStateOf(true) private var lastBackgroundTime: Long = 0L private var hasBeenBackgrounded = false override fun onCreate(savedInstanceState: Bundle?) { // Install splash screen before super.onCreate installSplashScreen() super.onCreate(savedInstanceState) // Enable edge-to-edge mode for proper keyboard (IME) insets detection enableEdgeToEdge() WindowCompat.setDecorFitsSystemWindows(window, false) // On cold start, require auth if biometric + requireOnStartup are enabled if (securityPreferences.isBiometricEnabled.value && securityPreferences.requireOnStartup.value) { isAuthenticated = false } // Parse intent ONCE, before composition (avoids re-execution on recomposition) val shareData = parseShareIntent(intent) // Check if user is already logged in to skip login screen for share intents val hasValidToken = tokenManager.getToken() != null && tokenManager.getBaseUrl() != null val startDestination = if (hasValidToken) { com.shaarit.presentation.nav.Screen.Feed.createRoute() } else { com.shaarit.presentation.nav.Screen.Login.route } setContent { val currentTheme by themePreferences.currentTheme.collectAsState() val currentThemeMode by themePreferences.themeMode.collectAsState() val biometricEnabled by securityPreferences.isBiometricEnabled.collectAsState() val needsAuth = biometricEnabled && !isAuthenticated ShaarItTheme(appTheme = currentTheme, themeMode = currentThemeMode) { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { if (needsAuth) { LockScreen( activity = this@MainActivity, biometricAuthManager = biometricAuthManager, onAuthenticated = { isAuthenticated = true } ) } else { val playerState by audioHandler.playerState.collectAsState() var showFullPlayer by androidx.compose.runtime.remember { mutableStateOf(false) } Box(modifier = Modifier.fillMaxSize()) { AppNavGraph( startDestination = startDestination, shareUrl = shareData.url, shareTitle = shareData.title, shareDescription = shareData.description, shareTags = shareData.tags, isFileShare = shareData.isFileShare, initialDeepLink = shareData.deepLink, onPlayAudio = { link -> val contentType = when { link.tags.any { it.equals("radio", true) } -> AudioContentType.RADIO link.tags.any { it.equals("podcast", true) } -> AudioContentType.PODCAST else -> AudioContentType.MUSIC } audioHandler.play( AudioMedia( id = link.id, title = link.title, url = link.url, artist = link.siteName, thumbnailUrl = link.thumbnailUrl, contentType = contentType ) ) } ) MiniPlayerBar( playerState = playerState, onTogglePlayPause = { audioHandler.togglePlayPause() }, onClose = { audioHandler.stop() }, onClick = { showFullPlayer = true }, modifier = Modifier .align(Alignment.BottomCenter) .navigationBarsPadding() ) } if (showFullPlayer && playerState.isActive) { FullPlayerSheet( playerState = playerState, onDismiss = { showFullPlayer = false }, onTogglePlayPause = { audioHandler.togglePlayPause() }, onSeek = { fraction -> audioHandler.seekToFraction(fraction) }, onNext = { audioHandler.next() }, onPrevious = { audioHandler.previous() }, onClose = { audioHandler.stop() showFullPlayer = false } ) } } } } } } override fun onStart() { super.onStart() audioHandler.connect() } override fun onStop() { super.onStop() audioHandler.disconnect() lastBackgroundTime = System.currentTimeMillis() hasBeenBackgrounded = true } override fun onResume() { super.onResume() if (!hasBeenBackgrounded) return if (securityPreferences.isBiometricEnabled.value) { val elapsed = System.currentTimeMillis() - lastBackgroundTime val timeout = securityPreferences.lockTimeout.value.delayMs val shouldLock = if (securityPreferences.requireOnResume.value) { lastBackgroundTime > 0 && elapsed > timeout } else { false } if (shouldLock) { isAuthenticated = false } } } private data class ShareData( val url: String? = null, val title: String? = null, val description: String? = null, val tags: List? = null, val isFileShare: Boolean = false, val deepLink: String? = null ) private fun parseShareIntent(intent: android.content.Intent?): ShareData { if (intent == null) return ShareData() var shareUrl: String? = null var shareTitle: String? = null var shareDescription: String? = null var shareTags: List? = null var isFileShare = false var deepLink: String? = null // Handle share intent if (intent.action == android.content.Intent.ACTION_SEND) { val mimeType = intent.type ?: "" // Check if this is a file share (markdown or text file) val fileUri = intent.getParcelableExtra(android.content.Intent.EXTRA_STREAM) if (fileUri != null && isTextOrMarkdownFile(mimeType, fileUri)) { isFileShare = true val fileInfo = readFileContent(fileUri) shareTitle = fileInfo.first shareDescription = fileInfo.second shareTags = listOf("note", "fichier") shareUrl = null } else if (mimeType == "text/plain") { shareUrl = intent.getStringExtra(android.content.Intent.EXTRA_TEXT) shareTitle = intent.getStringExtra(android.content.Intent.EXTRA_SUBJECT) } } // Handle deep links from App Shortcuts intent.data?.let { uri -> if (uri.scheme == "shaarit") { deepLink = uri.toString() } } return ShareData(shareUrl, shareTitle, shareDescription, shareTags, isFileShare, deepLink) } /** * Checks if the shared content is a text or markdown file */ private fun isTextOrMarkdownFile(mimeType: String, uri: Uri): Boolean { val isTextMime = mimeType.startsWith("text/") || mimeType == "application/octet-stream" val filename = getFileName(uri)?.lowercase() ?: "" val isMarkdownOrText = filename.endsWith(".md") || filename.endsWith(".markdown") || filename.endsWith(".txt") || filename.endsWith(".text") return isTextMime && (isMarkdownOrText || mimeType.contains("markdown")) } /** * Reads the content of a file and returns (filename without extension, content) */ private fun readFileContent(uri: Uri): Pair { val filename = getFileName(uri) ?: "Note" val filenameWithoutExtension = filename.substringBeforeLast(".") val content = try { contentResolver.openInputStream(uri)?.use { inputStream -> BufferedReader(InputStreamReader(inputStream)).use { reader -> reader.readText() } } ?: "" } catch (e: Exception) { "" } return Pair(filenameWithoutExtension, content) } /** * Gets the filename from a Uri */ private fun getFileName(uri: Uri): String? { return try { contentResolver.query(uri, null, null, null, null)?.use { cursor -> val nameIndex = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME) if (nameIndex >= 0 && cursor.moveToFirst()) { cursor.getString(nameIndex) } else { uri.lastPathSegment } } ?: uri.lastPathSegment } catch (e: Exception) { uri.lastPathSegment } } }