Fix shared contact deeplink (#3302)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Co-authored-by: James Rich <2199651+jamesarich@users.noreply.github.com>
pull/3305/head
Phil Oliver 2025-10-03 06:42:52 -04:00 zatwierdzone przez GitHub
rodzic 2accdd7f77
commit 5d95dca354
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
11 zmienionych plików z 153 dodań i 52 usunięć

Wyświetl plik

@ -66,6 +66,7 @@
<ID>FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
<ID>ForbiddenComment:SafeBluetooth.kt$SafeBluetooth$// TODO: display some kind of UI about restarting BLE</ID>
<ID>LambdaParameterEventTrailing:Channel.kt$onConfirm</ID>
<ID>LambdaParameterEventTrailing:ContactSharing.kt$onSharedContactRequested</ID>
<ID>LambdaParameterEventTrailing:Message.kt$onClick</ID>
<ID>LambdaParameterEventTrailing:Message.kt$onSendMessage</ID>
<ID>LambdaParameterEventTrailing:MessageList.kt$onReply</ID>
@ -178,6 +179,7 @@
<ID>ModifierMissing:SecurityConfigItemList.kt$SecurityConfigScreen</ID>
<ID>ModifierMissing:SettingsScreen.kt$SettingsScreen</ID>
<ID>ModifierMissing:Share.kt$ShareScreen</ID>
<ID>ModifierMissing:SharedContactDialog.kt$SharedContactDialog</ID>
<ID>ModifierMissing:SignalMetrics.kt$SignalMetricsScreen</ID>
<ID>ModifierMissing:TopLevelNavIcon.kt$TopLevelNavIcon</ID>
<ID>ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)</ID>

Wyświetl plik

@ -42,7 +42,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.MainScreen
import com.geeksville.mesh.ui.intro.AppIntroductionScreen
import com.geeksville.mesh.ui.sharing.toSharedContact
import dagger.hilt.android.AndroidEntryPoint
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
@ -118,9 +117,8 @@ class MainActivity : AppCompatActivity() {
Timber.d("App link data is a channel set")
model.requestChannelUrl(it)
} else if (it.path?.startsWith("/v/") == true || it.path?.startsWith("/V/") == true) {
val sharedContact = it.toSharedContact()
Timber.d("App link data is a shared contact: ${sharedContact.user.longName}")
model.setSharedContactRequested(sharedContact)
Timber.d("App link data is a shared contact")
model.setSharedContactRequested(it)
} else {
Timber.d("App link data is not a channel set")
}

Wyświetl plik

@ -43,6 +43,7 @@ import com.geeksville.mesh.copy
import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.service.MeshServiceNotifications
import com.geeksville.mesh.ui.sharing.toSharedContact
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@ -297,8 +298,17 @@ constructor(
val sharedContactRequested: StateFlow<AdminProtos.SharedContact?>
get() = _sharedContactRequested.asStateFlow()
fun setSharedContactRequested(sharedContact: AdminProtos.SharedContact?) {
_sharedContactRequested.value = sharedContact
fun setSharedContactRequested(url: Uri) {
runCatching { _sharedContactRequested.value = url.toSharedContact() }
.onFailure { ex ->
Timber.e(ex, "Shared contact error")
showSnackBar(R.string.contact_invalid)
}
}
/** Called immediately after activity observes requestChannelUrl */
fun clearSharedContactRequested() {
_sharedContactRequested.value = null
}
// Connection state to our radio device

Wyświetl plik

@ -91,6 +91,7 @@ import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
import com.geeksville.mesh.ui.connections.DeviceType
import com.geeksville.mesh.ui.connections.components.TopLevelNavIcon
import com.geeksville.mesh.ui.metrics.annotateTraceroute
import com.geeksville.mesh.ui.sharing.SharedContactDialog
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@ -139,6 +140,7 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
val navController = rememberNavController()
val connectionState by uIViewModel.connectionState.collectAsStateWithLifecycle()
val requestChannelSet by uIViewModel.requestChannelSet.collectAsStateWithLifecycle()
val sharedContactRequested by uIViewModel.sharedContactRequested.collectAsStateWithLifecycle()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val notificationPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
@ -150,6 +152,10 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
}
if (connectionState == ConnectionState.CONNECTED) {
sharedContactRequested?.let {
SharedContactDialog(sharedContact = it, onDismiss = { uIViewModel.clearSharedContactRequested() })
}
requestChannelSet?.let { newChannelSet ->
ScannedQrCodeDialog(newChannelSet, onDismiss = { uIViewModel.clearRequestChannelUrl() })
}

Wyświetl plik

@ -109,17 +109,15 @@ fun NodeListScreen(viewModel: NodeListViewModel = hiltViewModel(), navigateToNod
floatingActionButton = {
val firmwareVersion = DeviceVersion(ourNode?.metadata?.firmwareVersion ?: "0.0.0")
val shareCapable = firmwareVersion.supportsQrCodeSharing()
val scannedContact: AdminProtos.SharedContact? by
val sharedContact: AdminProtos.SharedContact? by
viewModel.sharedContactRequested.collectAsStateWithLifecycle(null)
AddContactFAB(
unfilteredNodes = unfilteredNodes,
scannedContact = scannedContact,
sharedContact = sharedContact,
modifier =
Modifier.animateFloatingActionButton(
visible = !isScrollInProgress && connectionState == ConnectionState.CONNECTED && shareCapable,
alignment = Alignment.BottomEnd,
),
onSharedContactImport = { contact -> viewModel.addSharedContact(contact) },
onSharedContactRequested = { contact -> viewModel.setSharedContactRequested(contact) },
)
},

Wyświetl plik

@ -30,9 +30,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.QrCodeScanner
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -72,17 +70,14 @@ import java.net.MalformedURLException
* requests using Accompanist Permissions.
*
* @param modifier Modifier for this composable.
* @param onSharedContactImport Callback invoked when a shared contact is successfully imported.
*/
@OptIn(ExperimentalPermissionsApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun AddContactFAB(
unfilteredNodes: List<Node>,
scannedContact: AdminProtos.SharedContact?,
sharedContact: AdminProtos.SharedContact?,
modifier: Modifier = Modifier,
onSharedContactImport: (AdminProtos.SharedContact) -> Unit = {},
onSharedContactRequested: (AdminProtos.SharedContact?) -> Unit = {},
onSharedContactRequested: (AdminProtos.SharedContact?) -> Unit,
) {
val barcodeLauncher =
rememberLauncherForActivityResult(ScanContract()) { result ->
@ -101,37 +96,7 @@ fun AddContactFAB(
}
}
scannedContact?.let { contactToImport ->
val nodeNum = contactToImport.nodeNum
val node = unfilteredNodes.find { it.num == nodeNum }
SimpleAlertDialog(
title = R.string.import_shared_contact,
text = {
Column {
if (node != null) {
Text(text = stringResource(R.string.import_known_shared_contact_text))
if (node.user.publicKey.size() > 0 && node.user.publicKey != contactToImport.user?.publicKey) {
Text(
text = stringResource(R.string.public_key_changed),
color = MaterialTheme.colorScheme.error,
)
}
HorizontalDivider()
Text(text = compareUsers(node.user, contactToImport.user))
} else {
Text(text = userFieldsToString(contactToImport.user))
}
}
},
dismissText = stringResource(R.string.cancel),
onDismiss = { onSharedContactRequested(null) },
confirmText = stringResource(R.string.import_label),
onConfirm = {
onSharedContactImport(contactToImport)
onSharedContactRequested(null)
},
)
}
sharedContact?.let { SharedContactDialog(sharedContact = it, onDismiss = { onSharedContactRequested(null) }) }
fun zxingScan() {
Timber.d("Starting zxing QR code scanner")

Wyświetl plik

@ -0,0 +1,72 @@
/*
* Copyright (c) 2025 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* 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.ui.sharing
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AdminProtos
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SimpleAlertDialog
/** A dialog for importing a shared contact that was scanned from a QR code. */
@Composable
fun SharedContactDialog(
sharedContact: AdminProtos.SharedContact,
onDismiss: () -> Unit,
viewModel: SharedContactViewModel = hiltViewModel(),
) {
val unfilteredNodes by viewModel.unfilteredNodes.collectAsStateWithLifecycle()
val nodeNum = sharedContact.nodeNum
val node = unfilteredNodes.find { it.num == nodeNum }
SimpleAlertDialog(
title = R.string.import_shared_contact,
text = {
Column {
if (node != null) {
Text(text = stringResource(R.string.import_known_shared_contact_text))
if (node.user.publicKey.size() > 0 && node.user.publicKey != sharedContact.user?.publicKey) {
Text(
text = stringResource(R.string.public_key_changed),
color = MaterialTheme.colorScheme.error,
)
}
HorizontalDivider()
Text(text = compareUsers(node.user, sharedContact.user))
} else {
Text(text = userFieldsToString(sharedContact.user))
}
}
},
dismissText = stringResource(R.string.cancel),
onDismiss = onDismiss,
confirmText = stringResource(R.string.import_label),
onConfirm = {
viewModel.addSharedContact(sharedContact)
onDismiss()
},
)
}

Wyświetl plik

@ -0,0 +1,53 @@
/*
* Copyright (c) 2025 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* 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.ui.sharing
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.AdminProtos
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.service.ServiceRepository
import javax.inject.Inject
@HiltViewModel
class SharedContactViewModel
@Inject
constructor(
nodeRepository: NodeRepository,
private val serviceRepository: ServiceRepository,
) : ViewModel() {
val unfilteredNodes: StateFlow<List<Node>> =
nodeRepository
.getNodes()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList(),
)
fun addSharedContact(sharedContact: AdminProtos.SharedContact) =
viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.ImportContact(sharedContact)) }
}

Wyświetl plik

@ -214,6 +214,7 @@
<string name="meshtastic_service_notifications">Service notifications</string>
<string name="about">About</string>
<string name="channel_invalid">This Channel URL is invalid and can not be used</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Debug Panel</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>

Wyświetl plik

@ -14,6 +14,5 @@
<ID>ParameterNaming:NodeFilterTextField.kt$onToggleShowIgnored</ID>
<ID>PreviewPublic:NodeItem.kt$NodeInfoPreview</ID>
<ID>PreviewPublic:NodeItem.kt$NodeInfoSimplePreview</ID>
<ID>TooManyFunctions:NodeListViewModel.kt$NodeListViewModel : ViewModel</ID>
</CurrentIssues>
</SmellBaseline>

Wyświetl plik

@ -161,9 +161,6 @@ constructor(
uiPreferencesDataSource.setNodeSort(sort.ordinal)
}
fun addSharedContact(sharedContact: AdminProtos.SharedContact) =
viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.ImportContact(sharedContact)) }
fun setSharedContactRequested(sharedContact: AdminProtos.SharedContact?) {
_sharedContactRequested.value = sharedContact
}