mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat(nsd): Add support for Android 14+ NSD resolving (#3731)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
deedd00995
commit
7feab79da3
8 changed files with 124 additions and 62 deletions
|
|
@ -17,57 +17,63 @@
|
|||
|
||||
package com.geeksville.mesh.repository.network
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import android.os.Build
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal fun NsdManager.serviceList(
|
||||
serviceType: String,
|
||||
): Flow<List<NsdServiceInfo>> = discoverServices(serviceType).mapLatest { serviceList ->
|
||||
serviceList
|
||||
.mapNotNull { resolveService(it) }
|
||||
}
|
||||
internal fun NsdManager.serviceList(serviceType: String): Flow<List<NsdServiceInfo>> =
|
||||
discoverServices(serviceType).mapLatest { serviceList -> serviceList.mapNotNull { resolveService(it) } }
|
||||
|
||||
private fun NsdManager.discoverServices(
|
||||
serviceType: String,
|
||||
protocolType: Int = NsdManager.PROTOCOL_DNS_SD,
|
||||
): Flow<List<NsdServiceInfo>> = callbackFlow {
|
||||
val serviceList = CopyOnWriteArrayList<NsdServiceInfo>()
|
||||
val discoveryListener = object : NsdManager.DiscoveryListener {
|
||||
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||
cancel("Start Discovery failed: Error code: $errorCode")
|
||||
}
|
||||
val discoveryListener =
|
||||
object : NsdManager.DiscoveryListener {
|
||||
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||
cancel("Start Discovery failed: Error code: $errorCode")
|
||||
}
|
||||
|
||||
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||
cancel("Stop Discovery failed: Error code: $errorCode")
|
||||
}
|
||||
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||
cancel("Stop Discovery failed: Error code: $errorCode")
|
||||
}
|
||||
|
||||
override fun onDiscoveryStarted(serviceType: String) {
|
||||
}
|
||||
override fun onDiscoveryStarted(serviceType: String) {
|
||||
Timber.d("NSD Service discovery started")
|
||||
}
|
||||
|
||||
override fun onDiscoveryStopped(serviceType: String) {
|
||||
close()
|
||||
}
|
||||
override fun onDiscoveryStopped(serviceType: String) {
|
||||
Timber.d("NSD Service discovery stopped")
|
||||
close()
|
||||
}
|
||||
|
||||
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
|
||||
serviceList += serviceInfo
|
||||
trySend(serviceList)
|
||||
}
|
||||
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
|
||||
Timber.d("NSD Service found: $serviceInfo")
|
||||
serviceList += serviceInfo
|
||||
trySend(serviceList)
|
||||
}
|
||||
|
||||
override fun onServiceLost(serviceInfo: NsdServiceInfo) {
|
||||
serviceList.removeAll { it.serviceName == serviceInfo.serviceName }
|
||||
trySend(serviceList)
|
||||
override fun onServiceLost(serviceInfo: NsdServiceInfo) {
|
||||
Timber.d("NSD Service lost: $serviceInfo")
|
||||
serviceList.removeAll { it.serviceName == serviceInfo.serviceName }
|
||||
trySend(serviceList)
|
||||
}
|
||||
}
|
||||
}
|
||||
trySend(emptyList()) // Emit an initial empty list
|
||||
discoverServices(serviceType, protocolType, discoveryListener)
|
||||
|
||||
|
|
@ -80,17 +86,60 @@ private fun NsdManager.discoverServices(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun NsdManager.resolveService(
|
||||
serviceInfo: NsdServiceInfo,
|
||||
): NsdServiceInfo? = suspendCancellableCoroutine { continuation ->
|
||||
val listener = object : NsdManager.ResolveListener {
|
||||
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
||||
continuation.resume(null)
|
||||
}
|
||||
@SuppressLint("NewApi")
|
||||
private suspend fun NsdManager.resolveService(serviceInfo: NsdServiceInfo): NsdServiceInfo? =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
val callback =
|
||||
object : NsdManager.ServiceInfoCallback {
|
||||
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
|
||||
continuation.resume(null)
|
||||
}
|
||||
|
||||
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
|
||||
continuation.resume(serviceInfo)
|
||||
override fun onServiceUpdated(updatedServiceInfo: NsdServiceInfo) {
|
||||
if (updatedServiceInfo.hostAddresses.isNotEmpty()) {
|
||||
continuation.resume(updatedServiceInfo)
|
||||
try {
|
||||
unregisterServiceInfoCallback(this)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.w(e, "Already unregistered")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceLost() {
|
||||
continuation.resume(null)
|
||||
try {
|
||||
unregisterServiceInfoCallback(this)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.w(e, "Already unregistered")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceInfoCallbackUnregistered() {
|
||||
// No op
|
||||
}
|
||||
}
|
||||
registerServiceInfoCallback(serviceInfo, Dispatchers.Main.asExecutor(), callback)
|
||||
continuation.invokeOnCancellation {
|
||||
try {
|
||||
unregisterServiceInfoCallback(callback)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.w(e, "Already unregistered")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val listener =
|
||||
object : NsdManager.ResolveListener {
|
||||
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
||||
continuation.resume(null)
|
||||
}
|
||||
|
||||
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
|
||||
continuation.resume(serviceInfo)
|
||||
}
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
resolveService(serviceInfo, listener)
|
||||
}
|
||||
}
|
||||
resolveService(serviceInfo, listener)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ abstract class StreamInterface(protected val service: RadioInterfaceService) : I
|
|||
|
||||
/** Print device serial debug output somewhere */
|
||||
private fun debugOut(b: Byte) {
|
||||
when (val c = b.toChar()) {
|
||||
when (val c = b.toInt().toChar()) {
|
||||
'\r' -> {} // ignore
|
||||
'\n' -> {
|
||||
Timber.d("DeviceLog: $debugLineBuf")
|
||||
|
|
|
|||
|
|
@ -2242,15 +2242,6 @@ class MeshService : Service() {
|
|||
rememberReaction(packet.copy { from = myNodeNum })
|
||||
}
|
||||
|
||||
fun clearDatabases() = serviceScope.handledLaunch {
|
||||
Timber.d("Clearing nodeDB")
|
||||
discardNodeDB()
|
||||
nodeRepository.clearNodeDB()
|
||||
|
||||
Timber.d("Clearing packetDB")
|
||||
packetRepository.get().clearPacketDB()
|
||||
}
|
||||
|
||||
private fun updateLastAddress(deviceAddr: String?) {
|
||||
val currentAddr = meshPrefs.deviceAddress
|
||||
Timber.d("setDeviceAddress: received request to change to: ${deviceAddr.anonymize}")
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import androidx.compose.material3.LocalContentColor
|
|||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TooltipAnchorPosition
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
|
|
@ -292,7 +293,8 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
|
|||
item(
|
||||
icon = {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
|
||||
positionProvider =
|
||||
TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import com.geeksville.mesh.model.DeviceListEntry
|
|||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.MultiplePermissionsState
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
|
|
@ -98,19 +99,42 @@ fun BLEDevices(
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
listOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
|
||||
} else {
|
||||
listOf(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
listOf(
|
||||
Manifest.permission.BLUETOOTH,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val permsMissing = stringResource(Res.string.permission_missing)
|
||||
val permsMissing =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
stringResource(Res.string.permission_missing_31)
|
||||
} else {
|
||||
stringResource(Res.string.permission_missing)
|
||||
}
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val singlePermissionState =
|
||||
rememberPermissionState(
|
||||
permission = Manifest.permission.ACCESS_BACKGROUND_LOCATION,
|
||||
onPermissionResult = { granted ->
|
||||
scanModel.refreshPermissions()
|
||||
scanModel.startScan()
|
||||
},
|
||||
)
|
||||
|
||||
val permissionsState =
|
||||
rememberMultiplePermissionsState(
|
||||
permissions = bluetoothPermissionsList,
|
||||
onPermissionsResult = {
|
||||
if (it.values.all { granted -> granted } && bluetoothEnabled) {
|
||||
onPermissionsResult = { permissions ->
|
||||
val granted = permissions.values.all { it }
|
||||
if (permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false)) {
|
||||
coroutineScope.launch { context.showToast(permsMissing) }
|
||||
singlePermissionState.launchPermissionRequest()
|
||||
}
|
||||
if (granted) {
|
||||
scanModel.refreshPermissions()
|
||||
scanModel.startScan()
|
||||
} else {
|
||||
|
|
@ -121,8 +145,8 @@ fun BLEDevices(
|
|||
|
||||
val settingsLauncher =
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {
|
||||
// Eventually auto scan once bluetooth is available
|
||||
// checkPermissionsAndScan(permissionsState, scanModel, bluetoothEnabled)
|
||||
scanModel.refreshPermissions()
|
||||
scanModel.startScan()
|
||||
}
|
||||
|
||||
Column(
|
||||
|
|
|
|||
|
|
@ -155,11 +155,7 @@ fun ContactsScreen(
|
|||
// if it's a node, look up the nodeNum including the !
|
||||
val nodeKey = contact.contactKey.substring(1)
|
||||
val node = viewModel.getNode(nodeKey)
|
||||
|
||||
if (node != null) {
|
||||
// navigate to node details.
|
||||
onNavigateToNodeDetails(node.num)
|
||||
}
|
||||
onNavigateToNodeDetails(node.num)
|
||||
} else {
|
||||
// Channels
|
||||
}
|
||||
|
|
|
|||
|
|
@ -363,12 +363,12 @@ fun ChannelScreen(
|
|||
item {
|
||||
PreferenceFooter(
|
||||
enabled = enabled,
|
||||
negativeText = Res.string.reset,
|
||||
negativeText = stringResource(Res.string.reset),
|
||||
onNegativeClicked = {
|
||||
focusManager.clearFocus()
|
||||
showResetDialog = true
|
||||
},
|
||||
positiveText = Res.string.scan,
|
||||
positiveText = stringResource(Res.string.scan),
|
||||
onPositiveClicked = {
|
||||
focusManager.clearFocus()
|
||||
if (cameraPermissionState.status.isGranted) {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ constructor(
|
|||
|
||||
// managed mode disables all access to configuration
|
||||
val isManaged: Boolean
|
||||
get() = localConfig.value.device.isManaged || localConfig.value.security.isManaged
|
||||
get() = localConfig.value.security.isManaged
|
||||
|
||||
var txEnabled: Boolean
|
||||
get() = localConfig.value.lora.txEnabled
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue