kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
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
rodzic
2accdd7f77
commit
5d95dca354
|
@ -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>
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() })
|
||||
}
|
||||
|
|
|
@ -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) },
|
||||
)
|
||||
},
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
|
@ -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)) }
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue