Fix config import from ACTION_VIEW (Android 16)

Handle content:// and file:// VIEW intents and add fallback octet-stream intent-filter
This commit is contained in:
世界
2026-02-04 17:53:17 +08:00
parent 46d2b6576c
commit 84dfd82ab8
2 changed files with 102 additions and 3 deletions

View File

@@ -91,6 +91,19 @@
<data android:pathPattern="/.*\\.bpf" />
</intent-filter>
<intent-filter android:priority="998">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.OPENABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="application/octet-stream" />
</intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>

View File

@@ -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<Triple<String, String, String>?>(null)
private var showImportLocalProfileDialog by mutableStateOf(false)
private var pendingImportLocalProfileName by mutableStateOf<String?>(null)
private var pendingImportLocalProfileUri by mutableStateOf<Uri?>(null)
private var newProfileArgs by mutableStateOf(NewProfileArgs())
private var parseImportLocalProfileJob: Job? = null
private var pendingIntentErrorMessage by mutableStateOf<String?>(null)
private val notificationPermissionLauncher =
registerForActivityResult(
@@ -222,8 +230,32 @@ 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<TopBarEntry>()) }
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) {