feat: Check if NFC is enabled and prompt user to enable it (#4482)

This commit is contained in:
James Rich 2026-02-06 12:08:35 -06:00 committed by GitHub
parent edd658f063
commit 1e255a5120
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 111 additions and 21 deletions

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh
import android.app.PendingIntent
@ -23,6 +22,8 @@ import android.content.Intent
import android.graphics.Color
import android.hardware.usb.UsbManager
import android.net.Uri
import android.nfc.NdefMessage
import android.nfc.NfcAdapter
import android.os.Build
import android.os.Bundle
import androidx.activity.SystemBarStyle
@ -46,6 +47,7 @@ import com.geeksville.mesh.ui.MainScreen
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.model.util.handleMeshtasticUri
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.channel_invalid
@ -111,28 +113,24 @@ class MainActivity : AppCompatActivity() {
handleIntent(intent)
}
@Suppress("NestedBlockDepth")
private fun handleIntent(intent: Intent) {
val appLinkAction = intent.action
val appLinkData: Uri? = intent.data
when (appLinkAction) {
Intent.ACTION_VIEW -> {
appLinkData?.let {
Logger.d { "App link data: $it" }
if (it.path?.startsWith("/e/") == true || it.path?.startsWith("/E/") == true) {
Logger.d { "App link data is a channel set" }
model.requestChannelUrl(
url = it,
onFailure = { lifecycleScope.launch { showToast(Res.string.channel_invalid) } },
)
} else if (it.path?.startsWith("/v/") == true || it.path?.startsWith("/V/") == true) {
Logger.d { "App link data is a shared contact" }
model.setSharedContactRequested(
url = it,
onFailure = { lifecycleScope.launch { showToast(Res.string.contact_invalid) } },
)
} else {
Logger.d { "App link data is not a channel set" }
appLinkData?.let { handleMeshtasticUri(it) }
}
NfcAdapter.ACTION_NDEF_DISCOVERED -> {
val rawMessages = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
if (rawMessages != null) {
for (rawMsg in rawMessages) {
val msg = rawMsg as NdefMessage
for (record in msg.records) {
record.toUri()?.let { handleMeshtasticUri(it) }
}
}
}
}
@ -157,6 +155,25 @@ class MainActivity : AppCompatActivity() {
}
}
private fun handleMeshtasticUri(uri: Uri) {
Logger.d { "Handling Meshtastic URI: $uri" }
handleMeshtasticUri(
uri = uri,
onChannel = {
model.requestChannelUrl(
url = it,
onFailure = { lifecycleScope.launch { showToast(Res.string.channel_invalid) } },
)
},
onContact = {
model.setSharedContactRequested(
url = it,
onFailure = { lifecycleScope.launch { showToast(Res.string.contact_invalid) } },
)
},
)
}
private fun createShareIntent(message: String): PendingIntent {
val deepLink = "$DEEP_LINK_BASE_URI/share?message=$message"
val startActivityIntent =

View file

@ -28,7 +28,7 @@ import co.touchlab.kermit.Logger
import java.io.IOException
@Composable
fun NfcScannerEffect(onResult: (String?) -> Unit) {
fun NfcScannerEffect(onResult: (String?) -> Unit, onNfcDisabled: (() -> Unit)? = null) {
val context = LocalContext.current
val activity = context as? Activity ?: return
@ -37,6 +37,9 @@ fun NfcScannerEffect(onResult: (String?) -> Unit) {
DisposableEffect(nfcAdapter) {
if (nfcAdapter == null) {
onDispose {}
} else if (!nfcAdapter.isEnabled) {
onNfcDisabled?.invoke()
onDispose {}
} else {
val readerCallback = NfcAdapter.ReaderCallback { tag: Tag -> handleNfcTag(tag, onResult) }

View file

@ -12,7 +12,7 @@
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. See <https://www.gnu.org/licenses/>.
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
@ -1179,4 +1179,5 @@
<string name="share_channels_qr">Share Channels QR Code</string>
<string name="scan_nfc_text">Bring your device close to the NFC tag to scan.</string>
<string name="generate_qr_code">Generate QR Code</string>
<string name="nfc_disabled">NFC is disabled. Please enable it in system settings.</string>
</resources>

View file

@ -33,6 +33,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import org.jetbrains.compose.resources.stringResource
@ -41,7 +42,9 @@ import org.meshtastic.core.nfc.NfcScannerEffect
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.cancel
import org.meshtastic.core.strings.input_shared_contact_url
import org.meshtastic.core.strings.nfc_disabled
import org.meshtastic.core.strings.okay
import org.meshtastic.core.strings.open_settings
import org.meshtastic.core.strings.scan_channels_nfc
import org.meshtastic.core.strings.scan_channels_qr
import org.meshtastic.core.strings.scan_nfc
@ -52,6 +55,7 @@ import org.meshtastic.core.strings.share_channels_qr
import org.meshtastic.core.strings.url
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.QrCode2
import org.meshtastic.core.ui.util.openNfcSettings
/**
* Unified Floating Action Button for importing Meshtastic data (Contacts, Channels, etc.) via NFC, QR, or URL.
@ -72,6 +76,8 @@ fun ImportFab(
var expanded by remember { mutableStateOf(false) }
var showUrlDialog by remember { mutableStateOf(false) }
var isNfcScanning by remember { mutableStateOf(false) }
var showNfcDisabledDialog by remember { mutableStateOf(false) }
val context = LocalContext.current
val barcodeScanner =
rememberBarcodeScanner(
@ -91,10 +97,35 @@ fun ImportFab(
isNfcScanning = false
}
},
onNfcDisabled = {
isNfcScanning = false
showNfcDisabledDialog = true
},
)
NfcScanningDialog(onDismiss = { isNfcScanning = false })
}
if (showNfcDisabledDialog) {
AlertDialog(
onDismissRequest = { showNfcDisabledDialog = false },
title = { Text(stringResource(Res.string.scan_nfc)) },
text = { Text(stringResource(Res.string.nfc_disabled)) },
confirmButton = {
TextButton(
onClick = {
context.openNfcSettings()
showNfcDisabledDialog = false
},
) {
Text(stringResource(Res.string.open_settings))
}
},
dismissButton = {
TextButton(onClick = { showNfcDisabledDialog = false }) { Text(stringResource(Res.string.cancel)) }
},
)
}
if (showUrlDialog) {
InputUrlDialog(
title = stringResource(Res.string.input_shared_contact_url),

View file

@ -19,6 +19,8 @@ package org.meshtastic.core.ui.util
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.provider.Settings
import android.widget.Toast
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
@ -41,3 +43,8 @@ fun Context.findActivity(): Activity? = when (this) {
is ContextWrapper -> baseContext.findActivity()
else -> null
}
fun Context.openNfcSettings() {
val intent = Intent(Settings.ACTION_NFC_SETTINGS)
startActivity(intent)
}

View file

@ -21,16 +21,19 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@ -45,6 +48,7 @@ import org.meshtastic.core.model.util.handleMeshtasticUri
import org.meshtastic.core.nfc.NfcScannerEffect
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.advanced
import org.meshtastic.core.strings.cancel
import org.meshtastic.core.strings.config_network_eth_enabled_summary
import org.meshtastic.core.strings.config_network_udp_enabled_summary
import org.meshtastic.core.strings.config_network_wifi_enabled_summary
@ -57,9 +61,12 @@ import org.meshtastic.core.strings.gateway
import org.meshtastic.core.strings.ip
import org.meshtastic.core.strings.ipv4_mode
import org.meshtastic.core.strings.network
import org.meshtastic.core.strings.nfc_disabled
import org.meshtastic.core.strings.ntp_server
import org.meshtastic.core.strings.open_settings
import org.meshtastic.core.strings.password
import org.meshtastic.core.strings.rsyslog_server
import org.meshtastic.core.strings.scan_nfc
import org.meshtastic.core.strings.ssid
import org.meshtastic.core.strings.subnet
import org.meshtastic.core.strings.udp_enabled
@ -76,6 +83,7 @@ import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.SimpleAlertDialog
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.util.openNfcSettings
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.Config
@ -88,12 +96,35 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBac
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val networkConfig = state.radioConfig.network ?: Config.NetworkConfig()
val formState = rememberConfigState(initialValue = networkConfig)
val context = LocalContext.current
var showScanErrorDialog: Boolean by rememberSaveable { mutableStateOf(false) }
if (showScanErrorDialog) {
ScanErrorDialog { showScanErrorDialog = false }
}
var showNfcDisabledDialog: Boolean by rememberSaveable { mutableStateOf(false) }
if (showNfcDisabledDialog) {
AlertDialog(
onDismissRequest = { showNfcDisabledDialog = false },
title = { Text(stringResource(Res.string.scan_nfc)) },
text = { Text(stringResource(Res.string.nfc_disabled)) },
confirmButton = {
TextButton(
onClick = {
context.openNfcSettings()
showNfcDisabledDialog = false
},
) {
Text(stringResource(Res.string.open_settings))
}
},
dismissButton = {
TextButton(onClick = { showNfcDisabledDialog = false }) { Text(stringResource(Res.string.cancel)) }
},
)
}
val onResult: (String?) -> Unit = { contents ->
if (contents != null) {
val handled =
@ -115,7 +146,7 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBac
}
val barcodeScanner = rememberBarcodeScanner(onResult = onResult)
NfcScannerEffect(onResult = onResult)
NfcScannerEffect(onResult = onResult, onNfcDisabled = { showNfcDisabledDialog = true })
val focusManager = LocalFocusManager.current