diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index de27715..5d9d8ff 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -91,6 +91,19 @@ + + + + + + + + + + + + + diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/MainActivity.kt b/app/src/main/java/io/nekohasekai/sfa/compose/MainActivity.kt index 9a37232..5db48b4 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/MainActivity.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/MainActivity.kt @@ -2,7 +2,9 @@ package io.nekohasekai.sfa.compose import android.Manifest import android.annotation.SuppressLint +import android.content.ContentResolver import android.content.Intent +import android.net.Uri import android.net.VpnService import android.os.Build import android.os.Bundle @@ -99,6 +101,7 @@ import io.nekohasekai.sfa.compose.navigation.ProfileRoutes import io.nekohasekai.sfa.compose.navigation.SFANavHost import io.nekohasekai.sfa.compose.navigation.Screen import io.nekohasekai.sfa.compose.navigation.bottomNavigationScreens +import io.nekohasekai.sfa.compose.screen.configuration.ProfileImportHandler import io.nekohasekai.sfa.compose.screen.connections.ConnectionDetailsScreen import io.nekohasekai.sfa.compose.screen.connections.ConnectionsPage import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel @@ -134,7 +137,12 @@ class MainActivity : private var showBackgroundLocationDialog by mutableStateOf(false) private var showImportProfileDialog by mutableStateOf(false) private var pendingImportProfile by mutableStateOf?>(null) + private var showImportLocalProfileDialog by mutableStateOf(false) + private var pendingImportLocalProfileName by mutableStateOf(null) + private var pendingImportLocalProfileUri by mutableStateOf(null) private var newProfileArgs by mutableStateOf(NewProfileArgs()) + private var parseImportLocalProfileJob: Job? = null + private var pendingIntentErrorMessage by mutableStateOf(null) private val notificationPermissionLauncher = registerForActivityResult( @@ -222,10 +230,34 @@ class MainActivity : pendingImportProfile = Triple(profile.name, profile.host, profile.url) showImportProfileDialog = true } catch (e: Exception) { - lifecycleScope.launch { - GlobalEventBus.emit(UiEvent.ErrorMessage(e.message ?: "Failed to parse profile link")) - } + pendingIntentErrorMessage = e.message ?: "Failed to parse profile link" } + return + } + + if (intent.action == Intent.ACTION_VIEW && + (uri.scheme == ContentResolver.SCHEME_CONTENT || uri.scheme == ContentResolver.SCHEME_FILE) + ) { + parseImportLocalProfileJob?.cancel() + parseImportLocalProfileJob = + lifecycleScope.launch(Dispatchers.IO) { + val importHandler = ProfileImportHandler(this@MainActivity) + when (val result = importHandler.parseUri(uri)) { + is ProfileImportHandler.UriParseResult.Success -> { + withContext(Dispatchers.Main) { + pendingImportLocalProfileName = result.name + pendingImportLocalProfileUri = uri + showImportLocalProfileDialog = true + } + } + + is ProfileImportHandler.UriParseResult.Error -> { + withContext(Dispatchers.Main) { + pendingIntentErrorMessage = result.message + } + } + } + } } } @@ -279,6 +311,7 @@ class MainActivity : val currentDestination = navBackStackEntry?.destination val currentRoute = currentDestination?.route val scope = rememberCoroutineScope() + val importHandler = remember { ProfileImportHandler(this@MainActivity) } val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass val useNavigationRail = @@ -296,6 +329,14 @@ class MainActivity : // Error dialog state for UiEvent.ShowError var showErrorDialog by remember { mutableStateOf(false) } var errorMessage by remember { mutableStateOf("") } + val pendingIntentError = pendingIntentErrorMessage + LaunchedEffect(pendingIntentError) { + if (pendingIntentError != null) { + errorMessage = pendingIntentError + showErrorDialog = true + pendingIntentErrorMessage = null + } + } val topBarState = remember { mutableStateOf(emptyList()) } val topBarController = remember { TopBarController(topBarState) } val topBarOverride = topBarState.value.lastOrNull()?.content @@ -378,6 +419,51 @@ class MainActivity : ) } + if (showImportLocalProfileDialog && pendingImportLocalProfileUri != null && pendingImportLocalProfileName != null) { + val importName = pendingImportLocalProfileName!! + val importUri = pendingImportLocalProfileUri!! + AlertDialog( + onDismissRequest = { + showImportLocalProfileDialog = false + pendingImportLocalProfileName = null + pendingImportLocalProfileUri = null + }, + title = { Text(stringResource(R.string.import_profile_confirm_title)) }, + text = { Text(stringResource(R.string.import_profile_confirm_message, importName)) }, + confirmButton = { + TextButton(onClick = { + showImportLocalProfileDialog = false + pendingImportLocalProfileName = null + pendingImportLocalProfileUri = null + scope.launch { + when (val result = importHandler.importFromUri(importUri)) { + is ProfileImportHandler.ImportResult.Success -> { + navController.navigate(ProfileRoutes.editProfile(result.profile.id)) { + launchSingleTop = true + } + } + is ProfileImportHandler.ImportResult.Error -> { + errorMessage = result.message + showErrorDialog = true + } + } + } + }) { + Text(stringResource(R.string.import_action)) + } + }, + dismissButton = { + TextButton(onClick = { + showImportLocalProfileDialog = false + pendingImportLocalProfileName = null + pendingImportLocalProfileUri = null + }) { + Text(stringResource(R.string.cancel)) + } + }, + ) + } + // Handle update check prompt dialog (shown only once on first launch) var showUpdateCheckPrompt by remember { mutableStateOf(!Settings.updateCheckPrompted) } if (showUpdateCheckPrompt) {