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:
@@ -91,6 +91,19 @@
|
|||||||
<data android:pathPattern="/.*\\.bpf" />
|
<data android:pathPattern="/.*\\.bpf" />
|
||||||
</intent-filter>
|
</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>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package io.nekohasekai.sfa.compose
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
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.SFANavHost
|
||||||
import io.nekohasekai.sfa.compose.navigation.Screen
|
import io.nekohasekai.sfa.compose.navigation.Screen
|
||||||
import io.nekohasekai.sfa.compose.navigation.bottomNavigationScreens
|
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.ConnectionDetailsScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsPage
|
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsPage
|
||||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
|
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
|
||||||
@@ -134,7 +137,12 @@ class MainActivity :
|
|||||||
private var showBackgroundLocationDialog by mutableStateOf(false)
|
private var showBackgroundLocationDialog by mutableStateOf(false)
|
||||||
private var showImportProfileDialog by mutableStateOf(false)
|
private var showImportProfileDialog by mutableStateOf(false)
|
||||||
private var pendingImportProfile by mutableStateOf<Triple<String, String, String>?>(null)
|
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 newProfileArgs by mutableStateOf(NewProfileArgs())
|
||||||
|
private var parseImportLocalProfileJob: Job? = null
|
||||||
|
private var pendingIntentErrorMessage by mutableStateOf<String?>(null)
|
||||||
|
|
||||||
private val notificationPermissionLauncher =
|
private val notificationPermissionLauncher =
|
||||||
registerForActivityResult(
|
registerForActivityResult(
|
||||||
@@ -222,10 +230,34 @@ class MainActivity :
|
|||||||
pendingImportProfile = Triple(profile.name, profile.host, profile.url)
|
pendingImportProfile = Triple(profile.name, profile.host, profile.url)
|
||||||
showImportProfileDialog = true
|
showImportProfileDialog = true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
lifecycleScope.launch {
|
pendingIntentErrorMessage = e.message ?: "Failed to parse profile link"
|
||||||
GlobalEventBus.emit(UiEvent.ErrorMessage(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 currentDestination = navBackStackEntry?.destination
|
||||||
val currentRoute = currentDestination?.route
|
val currentRoute = currentDestination?.route
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val importHandler = remember { ProfileImportHandler(this@MainActivity) }
|
||||||
|
|
||||||
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
|
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
|
||||||
val useNavigationRail =
|
val useNavigationRail =
|
||||||
@@ -296,6 +329,14 @@ class MainActivity :
|
|||||||
// Error dialog state for UiEvent.ShowError
|
// Error dialog state for UiEvent.ShowError
|
||||||
var showErrorDialog by remember { mutableStateOf(false) }
|
var showErrorDialog by remember { mutableStateOf(false) }
|
||||||
var errorMessage by remember { mutableStateOf("") }
|
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 topBarState = remember { mutableStateOf(emptyList<TopBarEntry>()) }
|
||||||
val topBarController = remember { TopBarController(topBarState) }
|
val topBarController = remember { TopBarController(topBarState) }
|
||||||
val topBarOverride = topBarState.value.lastOrNull()?.content
|
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)
|
// Handle update check prompt dialog (shown only once on first launch)
|
||||||
var showUpdateCheckPrompt by remember { mutableStateOf(!Settings.updateCheckPrompted) }
|
var showUpdateCheckPrompt by remember { mutableStateOf(!Settings.updateCheckPrompted) }
|
||||||
if (showUpdateCheckPrompt) {
|
if (showUpdateCheckPrompt) {
|
||||||
|
|||||||
Reference in New Issue
Block a user