From 502e41733892c4f21545067c53cd84d930f0696c Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Thu, 2 Oct 2025 20:18:09 -0400 Subject: [PATCH 1/9] `ConnectionsScreen` available BLE devices (#3298) --- .../mesh/ui/connections/ConnectionsScreen.kt | 76 ++----------------- .../ui/connections/components/BLEDevices.kt | 52 +++++++++---- core/strings/src/main/res/values/strings.xml | 3 +- 3 files changed, 48 insertions(+), 83 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt index 6010b5eff..a601a1443 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt @@ -25,23 +25,18 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.selection.selectable -import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Language import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -49,14 +44,12 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.geeksville.mesh.ConfigProtos @@ -118,7 +111,8 @@ fun ConnectionsScreen( val regionUnset = config.lora.region == ConfigProtos.Config.LoRaConfig.RegionCode.UNSET val bluetoothRssi by connectionsViewModel.bluetoothRssi.collectAsStateWithLifecycle() - val bleDevices by scanModel.bleDevicesForUi.collectAsStateWithLifecycle() + val bondedBleDevices by scanModel.bleDevicesForUi.collectAsStateWithLifecycle() + val scannedBleDevices by scanModel.scanResult.observeAsState(emptyMap()) val discoveredTcpDevices by scanModel.discoveredTcpDevicesForUi.collectAsStateWithLifecycle() val recentTcpDevices by scanModel.recentTcpDevicesForUi.collectAsStateWithLifecycle() val usbDevices by scanModel.usbDevicesForUi.collectAsStateWithLifecycle() @@ -152,15 +146,6 @@ fun ConnectionsScreen( } } - // State for the device scan dialog - var showScanDialog by remember { mutableStateOf(false) } - val scanResults by scanModel.scanResult.observeAsState(emptyMap()) - - // Observe scan results to show the dialog - if (scanResults.isNotEmpty()) { - showScanDialog = true - } - LaunchedEffect(connectionState, regionUnset) { when (connectionState) { ConnectionState.CONNECTED -> { @@ -245,7 +230,11 @@ fun ConnectionsScreen( DeviceType.BLE -> { BLEDevices( connectionState = connectionState, - btDevices = bleDevices, + bondedDevices = bondedBleDevices, + availableDevices = + scannedBleDevices.values.toList().filterNot { available -> + bondedBleDevices.any { it.address == available.address } + }, selectedDevice = selectedDevice, scanModel = scanModel, bluetoothEnabled = bluetoothState.enabled, @@ -280,7 +269,7 @@ fun ConnectionsScreen( val showWarningNotPaired = !connectionState.isConnected() && !hasShownNotPairedWarning && - bleDevices.none { it is DeviceListEntry.Ble && it.bonded } + bondedBleDevices.none { it is DeviceListEntry.Ble && it.bonded } if (showWarningNotPaired) { Text( text = stringResource(R.string.warning_not_paired), @@ -294,55 +283,6 @@ fun ConnectionsScreen( } } } - - // Compose Device Scan Dialog - if (showScanDialog) { - Dialog( - onDismissRequest = { - showScanDialog = false - scanModel.clearScanResults() - }, - ) { - Surface(shape = MaterialTheme.shapes.medium) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "Select a Bluetooth device", - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(bottom = 16.dp), - ) - Column(modifier = Modifier.selectableGroup()) { - scanResults.values.forEach { device -> - Row( - modifier = - Modifier.fillMaxWidth() - .selectable( - selected = false, // No pre-selection in this dialog - onClick = { - scanModel.onSelected(device) - scanModel.clearScanResults() - showScanDialog = false - }, - ) - .padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text(text = device.name) - } - } - } - Spacer(modifier = Modifier.height(16.dp)) - TextButton( - onClick = { - scanModel.clearScanResults() - showScanDialog = false - }, - ) { - Text(stringResource(R.string.cancel)) - } - } - } - } - } } Box(modifier = Modifier.fillMaxWidth().padding(8.dp)) { diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/components/BLEDevices.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/components/BLEDevices.kt index 38092eed9..bf9374b8d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/components/BLEDevices.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/components/BLEDevices.kt @@ -69,7 +69,8 @@ import org.meshtastic.core.ui.component.TitledCard @Composable fun BLEDevices( connectionState: ConnectionState, - btDevices: List, + bondedDevices: List, + availableDevices: List, selectedDevice: String, scanModel: BTScanModel, bluetoothEnabled: Boolean, @@ -153,7 +154,7 @@ fun BLEDevices( } } - if (btDevices.isEmpty()) { + if (bondedDevices.isEmpty() && availableDevices.isEmpty()) { EmptyStateContent( imageVector = Icons.Rounded.BluetoothDisabled, text = @@ -165,18 +166,19 @@ fun BLEDevices( actionButton = scanButton, ) } else { - TitledCard(title = stringResource(R.string.bluetooth_paired_devices)) { - btDevices.forEach { device -> - val connected = - connectionState == ConnectionState.CONNECTED && device.fullAddress == selectedDevice - DeviceListItem( - connected = connected, - device = device, - onSelect = { scanModel.onSelected(device) }, - modifier = Modifier, - ) - } - } + bondedDevices.Section( + title = stringResource(R.string.bluetooth_paired_devices), + connectionState = connectionState, + selectedDevice = selectedDevice, + onSelect = scanModel::onSelected, + ) + + availableDevices.Section( + title = stringResource(R.string.bluetooth_available_devices), + connectionState = connectionState, + selectedDevice = selectedDevice, + onSelect = scanModel::onSelected, + ) scanButton() } @@ -213,3 +215,25 @@ private fun checkPermissionsAndScan( permissionsState.launchMultiplePermissionRequest() } } + +@Composable +private fun List.Section( + title: String, + connectionState: ConnectionState, + selectedDevice: String, + onSelect: (DeviceListEntry) -> Unit, +) { + if (isNotEmpty()) { + TitledCard(title = title) { + forEach { device -> + val connected = connectionState == ConnectionState.CONNECTED && device.fullAddress == selectedDevice + DeviceListItem( + connected = connected, + device = device, + onSelect = { onSelect(device) }, + modifier = Modifier, + ) + } + } + } +} diff --git a/core/strings/src/main/res/values/strings.xml b/core/strings/src/main/res/values/strings.xml index d91d4913d..a231f5a4d 100644 --- a/core/strings/src/main/res/values/strings.xml +++ b/core/strings/src/main/res/values/strings.xml @@ -821,7 +821,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. From f854cafe6fa998858b737f7b3b7bab28857bd741 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Thu, 2 Oct 2025 19:40:45 -0500 Subject: [PATCH 2/9] New Crowdin updates (#3299) --- core/strings/src/main/res/values-ar-rSA/strings.xml | 3 ++- core/strings/src/main/res/values-b+sr+Latn/strings.xml | 3 ++- core/strings/src/main/res/values-bg-rBG/strings.xml | 3 ++- core/strings/src/main/res/values-ca-rES/strings.xml | 3 ++- core/strings/src/main/res/values-cs-rCZ/strings.xml | 3 ++- core/strings/src/main/res/values-de-rDE/strings.xml | 3 ++- core/strings/src/main/res/values-el-rGR/strings.xml | 3 ++- core/strings/src/main/res/values-es-rES/strings.xml | 3 ++- core/strings/src/main/res/values-et-rEE/strings.xml | 3 ++- core/strings/src/main/res/values-fi-rFI/strings.xml | 3 ++- core/strings/src/main/res/values-fr-rFR/strings.xml | 3 ++- core/strings/src/main/res/values-ga-rIE/strings.xml | 3 ++- core/strings/src/main/res/values-gl-rES/strings.xml | 3 ++- core/strings/src/main/res/values-hr-rHR/strings.xml | 3 ++- core/strings/src/main/res/values-ht-rHT/strings.xml | 3 ++- core/strings/src/main/res/values-hu-rHU/strings.xml | 3 ++- core/strings/src/main/res/values-is-rIS/strings.xml | 3 ++- core/strings/src/main/res/values-it-rIT/strings.xml | 3 ++- core/strings/src/main/res/values-iw-rIL/strings.xml | 3 ++- core/strings/src/main/res/values-ja-rJP/strings.xml | 3 ++- core/strings/src/main/res/values-ko-rKR/strings.xml | 3 ++- core/strings/src/main/res/values-lt-rLT/strings.xml | 3 ++- core/strings/src/main/res/values-nl-rNL/strings.xml | 3 ++- core/strings/src/main/res/values-no-rNO/strings.xml | 3 ++- core/strings/src/main/res/values-pl-rPL/strings.xml | 3 ++- core/strings/src/main/res/values-pt-rBR/strings.xml | 3 ++- core/strings/src/main/res/values-pt-rPT/strings.xml | 3 ++- core/strings/src/main/res/values-ro-rRO/strings.xml | 3 ++- core/strings/src/main/res/values-ru-rRU/strings.xml | 3 ++- core/strings/src/main/res/values-sk-rSK/strings.xml | 3 ++- core/strings/src/main/res/values-sl-rSI/strings.xml | 3 ++- core/strings/src/main/res/values-sq-rAL/strings.xml | 3 ++- core/strings/src/main/res/values-srp/strings.xml | 3 ++- core/strings/src/main/res/values-sv-rSE/strings.xml | 3 ++- core/strings/src/main/res/values-tr-rTR/strings.xml | 3 ++- core/strings/src/main/res/values-uk-rUA/strings.xml | 3 ++- core/strings/src/main/res/values-zh-rCN/strings.xml | 3 ++- core/strings/src/main/res/values-zh-rTW/strings.xml | 3 ++- 38 files changed, 76 insertions(+), 38 deletions(-) diff --git a/core/strings/src/main/res/values-ar-rSA/strings.xml b/core/strings/src/main/res/values-ar-rSA/strings.xml index 0367f95fb..114ea7485 100644 --- a/core/strings/src/main/res/values-ar-rSA/strings.xml +++ b/core/strings/src/main/res/values-ar-rSA/strings.xml @@ -794,7 +794,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-b+sr+Latn/strings.xml b/core/strings/src/main/res/values-b+sr+Latn/strings.xml index e84d446b7..e1b9dc1ab 100644 --- a/core/strings/src/main/res/values-b+sr+Latn/strings.xml +++ b/core/strings/src/main/res/values-b+sr+Latn/strings.xml @@ -788,7 +788,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-bg-rBG/strings.xml b/core/strings/src/main/res/values-bg-rBG/strings.xml index 394b429c3..6a2e93963 100644 --- a/core/strings/src/main/res/values-bg-rBG/strings.xml +++ b/core/strings/src/main/res/values-bg-rBG/strings.xml @@ -786,7 +786,8 @@ No PAX metrics logs available. WiFi устройства BLE устройства - Сдвоени устройства + Paired devices + Available devices Свързано устройство Rate Limit Exceeded. Please try again later. Преглед на изданието diff --git a/core/strings/src/main/res/values-ca-rES/strings.xml b/core/strings/src/main/res/values-ca-rES/strings.xml index 969ed8e0b..7cbdd992d 100644 --- a/core/strings/src/main/res/values-ca-rES/strings.xml +++ b/core/strings/src/main/res/values-ca-rES/strings.xml @@ -786,7 +786,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-cs-rCZ/strings.xml b/core/strings/src/main/res/values-cs-rCZ/strings.xml index 4a8e468d4..9f4131295 100644 --- a/core/strings/src/main/res/values-cs-rCZ/strings.xml +++ b/core/strings/src/main/res/values-cs-rCZ/strings.xml @@ -790,7 +790,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Spárovaná zařízení + Paired devices + Available devices Připojená zařízení Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-de-rDE/strings.xml b/core/strings/src/main/res/values-de-rDE/strings.xml index 0c8867119..21dd386b2 100644 --- a/core/strings/src/main/res/values-de-rDE/strings.xml +++ b/core/strings/src/main/res/values-de-rDE/strings.xml @@ -786,7 +786,8 @@ Keine Daten für den Besucherzähler verfügbar. WLAN Geräte BLE Geräte - Gekoppelte Geräte + Paired devices + Available devices Verbundene Geräte Sendebegrenzung überschritten. Bitte versuchen Sie es später erneut. Version ansehen diff --git a/core/strings/src/main/res/values-el-rGR/strings.xml b/core/strings/src/main/res/values-el-rGR/strings.xml index db3316144..78cd634e3 100644 --- a/core/strings/src/main/res/values-el-rGR/strings.xml +++ b/core/strings/src/main/res/values-el-rGR/strings.xml @@ -786,7 +786,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-es-rES/strings.xml b/core/strings/src/main/res/values-es-rES/strings.xml index 6f263dce1..a8cf9ae6d 100644 --- a/core/strings/src/main/res/values-es-rES/strings.xml +++ b/core/strings/src/main/res/values-es-rES/strings.xml @@ -788,7 +788,8 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-et-rEE/strings.xml b/core/strings/src/main/res/values-et-rEE/strings.xml index ccdf8b3a8..7a5dd68ec 100644 --- a/core/strings/src/main/res/values-et-rEE/strings.xml +++ b/core/strings/src/main/res/values-et-rEE/strings.xml @@ -786,7 +786,8 @@ PAX andurite logi ei ole saadaval. WiFi seadmed Sinihamba seadmed - Seotud seadmed + Paired devices + Available devices Ühendatud seadmed Limiit ületatud. Proovi hiljem uuesti. Näita versioon diff --git a/core/strings/src/main/res/values-fi-rFI/strings.xml b/core/strings/src/main/res/values-fi-rFI/strings.xml index 7f83d4e13..a39aafabf 100644 --- a/core/strings/src/main/res/values-fi-rFI/strings.xml +++ b/core/strings/src/main/res/values-fi-rFI/strings.xml @@ -786,7 +786,8 @@ PAX-laskurien lokitietoja ei ole saatavilla. WiFi-laitteet Bluetooth-laitteet - Paritetut laitteet + Paired devices + Available devices Yhdistetty laite Käyttöraja ylitetty. Yritä myöhemmin uudelleen. Näytä versio diff --git a/core/strings/src/main/res/values-fr-rFR/strings.xml b/core/strings/src/main/res/values-fr-rFR/strings.xml index d633b7c68..a4ef0dad8 100644 --- a/core/strings/src/main/res/values-fr-rFR/strings.xml +++ b/core/strings/src/main/res/values-fr-rFR/strings.xml @@ -786,7 +786,8 @@ Aucun journal de métriques PAX disponible. Périphériques WiFi Appareils BLE - Périphériques appairés + Paired devices + Available devices Périphérique connecté Limite de débit dépassée. Veuillez réessayer plus tard. Voir la version diff --git a/core/strings/src/main/res/values-ga-rIE/strings.xml b/core/strings/src/main/res/values-ga-rIE/strings.xml index 6aff2ce2a..5b8e5de45 100644 --- a/core/strings/src/main/res/values-ga-rIE/strings.xml +++ b/core/strings/src/main/res/values-ga-rIE/strings.xml @@ -792,7 +792,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-gl-rES/strings.xml b/core/strings/src/main/res/values-gl-rES/strings.xml index 0a9520442..22c7d6d1b 100644 --- a/core/strings/src/main/res/values-gl-rES/strings.xml +++ b/core/strings/src/main/res/values-gl-rES/strings.xml @@ -786,7 +786,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-hr-rHR/strings.xml b/core/strings/src/main/res/values-hr-rHR/strings.xml index 69bd710e8..a28d92dc9 100644 --- a/core/strings/src/main/res/values-hr-rHR/strings.xml +++ b/core/strings/src/main/res/values-hr-rHR/strings.xml @@ -788,7 +788,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-ht-rHT/strings.xml b/core/strings/src/main/res/values-ht-rHT/strings.xml index b6101ec7f..67e4e3818 100644 --- a/core/strings/src/main/res/values-ht-rHT/strings.xml +++ b/core/strings/src/main/res/values-ht-rHT/strings.xml @@ -786,7 +786,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-hu-rHU/strings.xml b/core/strings/src/main/res/values-hu-rHU/strings.xml index d9e08bde1..c4f92eafb 100644 --- a/core/strings/src/main/res/values-hu-rHU/strings.xml +++ b/core/strings/src/main/res/values-hu-rHU/strings.xml @@ -786,7 +786,8 @@ Nem érhetők el PAX metrika naplók. WiFi eszközök BLE eszközök - Párosított eszközök + Paired devices + Available devices Csatlakoztatott eszköz Túllépted a sebességkorlátot. Próbáld újra később. Kiadás megtekintése diff --git a/core/strings/src/main/res/values-is-rIS/strings.xml b/core/strings/src/main/res/values-is-rIS/strings.xml index 8b9fa7086..6d66ada36 100644 --- a/core/strings/src/main/res/values-is-rIS/strings.xml +++ b/core/strings/src/main/res/values-is-rIS/strings.xml @@ -786,7 +786,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-it-rIT/strings.xml b/core/strings/src/main/res/values-it-rIT/strings.xml index 896813575..16e88245b 100644 --- a/core/strings/src/main/res/values-it-rIT/strings.xml +++ b/core/strings/src/main/res/values-it-rIT/strings.xml @@ -786,7 +786,8 @@ Nessun log delle metriche PAX disponibile. Dispositivi WiFi Dispositivi BLE - Dispositivi associati + Paired devices + Available devices Dispositivo connesso Limite di trasmissione superato. Riprova più tardi Visualizza Release diff --git a/core/strings/src/main/res/values-iw-rIL/strings.xml b/core/strings/src/main/res/values-iw-rIL/strings.xml index 7f6b131ba..b3fe9f57c 100644 --- a/core/strings/src/main/res/values-iw-rIL/strings.xml +++ b/core/strings/src/main/res/values-iw-rIL/strings.xml @@ -790,7 +790,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-ja-rJP/strings.xml b/core/strings/src/main/res/values-ja-rJP/strings.xml index b4bfc71b8..03149f898 100644 --- a/core/strings/src/main/res/values-ja-rJP/strings.xml +++ b/core/strings/src/main/res/values-ja-rJP/strings.xml @@ -785,7 +785,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-ko-rKR/strings.xml b/core/strings/src/main/res/values-ko-rKR/strings.xml index 54e81fa96..44e734b48 100644 --- a/core/strings/src/main/res/values-ko-rKR/strings.xml +++ b/core/strings/src/main/res/values-ko-rKR/strings.xml @@ -784,7 +784,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-lt-rLT/strings.xml b/core/strings/src/main/res/values-lt-rLT/strings.xml index 0f8a606e1..65d4fa2e0 100644 --- a/core/strings/src/main/res/values-lt-rLT/strings.xml +++ b/core/strings/src/main/res/values-lt-rLT/strings.xml @@ -790,7 +790,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-nl-rNL/strings.xml b/core/strings/src/main/res/values-nl-rNL/strings.xml index 6fd61ce7c..60ac3c037 100644 --- a/core/strings/src/main/res/values-nl-rNL/strings.xml +++ b/core/strings/src/main/res/values-nl-rNL/strings.xml @@ -786,7 +786,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-no-rNO/strings.xml b/core/strings/src/main/res/values-no-rNO/strings.xml index fc7d14eb6..7cfc3539b 100644 --- a/core/strings/src/main/res/values-no-rNO/strings.xml +++ b/core/strings/src/main/res/values-no-rNO/strings.xml @@ -786,7 +786,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-pl-rPL/strings.xml b/core/strings/src/main/res/values-pl-rPL/strings.xml index 70e0ab3dc..133540c64 100644 --- a/core/strings/src/main/res/values-pl-rPL/strings.xml +++ b/core/strings/src/main/res/values-pl-rPL/strings.xml @@ -790,7 +790,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-pt-rBR/strings.xml b/core/strings/src/main/res/values-pt-rBR/strings.xml index 95473d853..d97e74a68 100644 --- a/core/strings/src/main/res/values-pt-rBR/strings.xml +++ b/core/strings/src/main/res/values-pt-rBR/strings.xml @@ -786,7 +786,8 @@ Não há logs de métricas de PAX disponíveis. Dispositivos WiFi Dispositivos BLE - Dispositivos Pareados + Paired devices + Available devices Dispositivo Conectado Limite excedido. Por favor, tente novamente mais tarde. Ver Lançamento diff --git a/core/strings/src/main/res/values-pt-rPT/strings.xml b/core/strings/src/main/res/values-pt-rPT/strings.xml index cf6036166..9b46f7b72 100644 --- a/core/strings/src/main/res/values-pt-rPT/strings.xml +++ b/core/strings/src/main/res/values-pt-rPT/strings.xml @@ -786,7 +786,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-ro-rRO/strings.xml b/core/strings/src/main/res/values-ro-rRO/strings.xml index daa7d77f4..a396602d9 100644 --- a/core/strings/src/main/res/values-ro-rRO/strings.xml +++ b/core/strings/src/main/res/values-ro-rRO/strings.xml @@ -788,7 +788,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-ru-rRU/strings.xml b/core/strings/src/main/res/values-ru-rRU/strings.xml index 84bc3268e..4795f79bb 100644 --- a/core/strings/src/main/res/values-ru-rRU/strings.xml +++ b/core/strings/src/main/res/values-ru-rRU/strings.xml @@ -790,7 +790,8 @@ Нет доступных журналов метрики пассажиров. WiFi устройства Устройства BLE - Сопряженные устройства + Paired devices + Available devices Подключённые устройства Превышен лимит запросов. Пожалуйста, повторите попытку позже. Просмотреть релиз diff --git a/core/strings/src/main/res/values-sk-rSK/strings.xml b/core/strings/src/main/res/values-sk-rSK/strings.xml index c915235da..68856abaf 100644 --- a/core/strings/src/main/res/values-sk-rSK/strings.xml +++ b/core/strings/src/main/res/values-sk-rSK/strings.xml @@ -790,7 +790,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-sl-rSI/strings.xml b/core/strings/src/main/res/values-sl-rSI/strings.xml index 031f652ad..4f5540f8e 100644 --- a/core/strings/src/main/res/values-sl-rSI/strings.xml +++ b/core/strings/src/main/res/values-sl-rSI/strings.xml @@ -790,7 +790,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-sq-rAL/strings.xml b/core/strings/src/main/res/values-sq-rAL/strings.xml index 24dd69e41..b93ee1020 100644 --- a/core/strings/src/main/res/values-sq-rAL/strings.xml +++ b/core/strings/src/main/res/values-sq-rAL/strings.xml @@ -786,7 +786,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-srp/strings.xml b/core/strings/src/main/res/values-srp/strings.xml index 7bd1b41a1..b5534392e 100644 --- a/core/strings/src/main/res/values-srp/strings.xml +++ b/core/strings/src/main/res/values-srp/strings.xml @@ -788,7 +788,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-sv-rSE/strings.xml b/core/strings/src/main/res/values-sv-rSE/strings.xml index d295146f4..aa78dc70e 100644 --- a/core/strings/src/main/res/values-sv-rSE/strings.xml +++ b/core/strings/src/main/res/values-sv-rSE/strings.xml @@ -786,7 +786,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-tr-rTR/strings.xml b/core/strings/src/main/res/values-tr-rTR/strings.xml index 5c37d639a..fc28b11bf 100644 --- a/core/strings/src/main/res/values-tr-rTR/strings.xml +++ b/core/strings/src/main/res/values-tr-rTR/strings.xml @@ -786,7 +786,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-uk-rUA/strings.xml b/core/strings/src/main/res/values-uk-rUA/strings.xml index 222cec9f3..d80268278 100644 --- a/core/strings/src/main/res/values-uk-rUA/strings.xml +++ b/core/strings/src/main/res/values-uk-rUA/strings.xml @@ -790,7 +790,8 @@ No PAX metrics logs available. WiFi Devices BLE Devices - Paired Devices + Paired devices + Available devices Connected Device Rate Limit Exceeded. Please try again later. View Release diff --git a/core/strings/src/main/res/values-zh-rCN/strings.xml b/core/strings/src/main/res/values-zh-rCN/strings.xml index 9b9b44bdb..988bdfca8 100644 --- a/core/strings/src/main/res/values-zh-rCN/strings.xml +++ b/core/strings/src/main/res/values-zh-rCN/strings.xml @@ -786,7 +786,8 @@ 无可用的 PAX 计量日志。 WiFi 设备 BLE 设备 - Paired Devices + Paired devices + Available devices Connected Device 超过速率限制。请稍后再试。 查看发行版 diff --git a/core/strings/src/main/res/values-zh-rTW/strings.xml b/core/strings/src/main/res/values-zh-rTW/strings.xml index 7d0fe935a..21da6cf59 100644 --- a/core/strings/src/main/res/values-zh-rTW/strings.xml +++ b/core/strings/src/main/res/values-zh-rTW/strings.xml @@ -784,7 +784,8 @@ 沒有可用的 PAX 指標日誌。 WiFi 裝置 藍牙裝置 - 配對裝置 + Paired devices + Available devices 連接裝置 超過速率限制,請稍後再嘗試。 查看版本資訊 From 4a8cd6fb41e14bad5cfacb356c62e9ba81ef85f8 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Fri, 3 Oct 2025 06:12:40 -0400 Subject: [PATCH 3/9] Decouple `ScannedQrCodeDialog` from `UiViewModel` (#3300) --- .../java/com/geeksville/mesh/model/UIState.kt | 17 ---- .../main/java/com/geeksville/mesh/ui/Main.kt | 4 +- .../common/components/ScannedQrCodeDialog.kt | 10 ++- .../components/ScannedQrCodeViewModel.kt | 86 +++++++++++++++++++ .../com/geeksville/mesh/ui/sharing/Channel.kt | 5 ++ .../mesh/ui/sharing/ChannelViewModel.kt | 4 + 6 files changed, 105 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeViewModel.kt diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 3ceeb7d29..e15419249 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -347,23 +347,6 @@ constructor( } } - fun setChannel(channel: ChannelProtos.Channel) { - try { - meshService?.setChannel(channel.toByteArray()) - } catch (ex: RemoteException) { - Timber.e(ex, "Set channel error") - } - } - - /** Set the radio config (also updates our saved copy in preferences). */ - fun setChannels(channelSet: AppOnlyProtos.ChannelSet) = viewModelScope.launch { - getChannelList(channelSet.settingsList, channels.value.settingsList).forEach(::setChannel) - radioConfigRepository.replaceAllSettings(channelSet.settingsList) - - val newConfig = config { lora = channelSet.loraConfig } - if (config.lora != newConfig.lora) setConfig(newConfig) - } - fun addQuickChatAction(action: QuickChatAction) = viewModelScope.launch(Dispatchers.IO) { quickChatActionRepository.upsert(action) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt index 0f79c842d..30fe3b5a7 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -150,7 +150,9 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode } if (connectionState == ConnectionState.CONNECTED) { - requestChannelSet?.let { newChannelSet -> ScannedQrCodeDialog(uIViewModel, newChannelSet) } + requestChannelSet?.let { newChannelSet -> + ScannedQrCodeDialog(newChannelSet, onDismiss = { uIViewModel.clearRequestChannelUrl() }) + } } analytics.addNavigationTrackingEffect(navController = navController) diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt index b600f1021..591d42435 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt @@ -49,24 +49,28 @@ import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.geeksville.mesh.AppOnlyProtos.ChannelSet import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig.ModemPreset import com.geeksville.mesh.channelSet import com.geeksville.mesh.copy -import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.ui.settings.radio.components.ChannelSelection import org.meshtastic.core.model.Channel import org.meshtastic.core.strings.R @Composable -fun ScannedQrCodeDialog(viewModel: UIViewModel, incoming: ChannelSet) { +fun ScannedQrCodeDialog( + incoming: ChannelSet, + onDismiss: () -> Unit, + viewModel: ScannedQrCodeViewModel = hiltViewModel(), +) { val channels by viewModel.channels.collectAsStateWithLifecycle() ScannedQrCodeDialog( channels = channels, incoming = incoming, - onDismiss = viewModel::clearRequestChannelUrl, + onDismiss = onDismiss, onConfirm = viewModel::setChannels, ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeViewModel.kt new file mode 100644 index 000000000..38b54cb8f --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeViewModel.kt @@ -0,0 +1,86 @@ +/* + * 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 . + */ + +package com.geeksville.mesh.ui.common.components + +import android.os.RemoteException +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.geeksville.mesh.AppOnlyProtos +import com.geeksville.mesh.ChannelProtos +import com.geeksville.mesh.ConfigProtos.Config +import com.geeksville.mesh.LocalOnlyProtos.LocalConfig +import com.geeksville.mesh.channelSet +import com.geeksville.mesh.config +import com.geeksville.mesh.model.getChannelList +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.meshtastic.core.data.repository.RadioConfigRepository +import org.meshtastic.core.service.ServiceRepository +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class ScannedQrCodeViewModel +@Inject +constructor( + private val radioConfigRepository: RadioConfigRepository, + private val serviceRepository: ServiceRepository, +) : ViewModel() { + + val channels = + radioConfigRepository.channelSetFlow.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000L), + channelSet {}, + ) + + private val localConfig = + radioConfigRepository.localConfigFlow.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000L), + LocalConfig.getDefaultInstance(), + ) + + /** Set the radio config (also updates our saved copy in preferences). */ + fun setChannels(channelSet: AppOnlyProtos.ChannelSet) = viewModelScope.launch { + getChannelList(channelSet.settingsList, channels.value.settingsList).forEach(::setChannel) + radioConfigRepository.replaceAllSettings(channelSet.settingsList) + + val newConfig = config { lora = channelSet.loraConfig } + if (localConfig.value.lora != newConfig.lora) setConfig(newConfig) + } + + private fun setChannel(channel: ChannelProtos.Channel) { + try { + serviceRepository.meshService?.setChannel(channel.toByteArray()) + } catch (ex: RemoteException) { + Timber.e(ex, "Set channel error") + } + } + + // Set the radio config (also updates our saved copy in preferences) + private fun setConfig(config: Config) { + try { + serviceRepository.meshService?.setConfig(config.toByteArray()) + } catch (ex: RemoteException) { + Timber.e(ex, "Set config error") + } + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt index 38d2f08e6..b7e2d0975 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt @@ -96,6 +96,7 @@ import com.geeksville.mesh.channelSet import com.geeksville.mesh.copy import com.geeksville.mesh.navigation.ConfigRoute import com.geeksville.mesh.navigation.getNavRouteFrom +import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import com.geeksville.mesh.ui.settings.radio.components.ChannelSelection import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog @@ -144,6 +145,8 @@ fun ChannelScreen( var shouldAddChannelsState by remember { mutableStateOf(true) } + val requestChannelSet by viewModel.requestChannelSet.collectAsStateWithLifecycle() + /* Animate waiting for the configurations */ var isWaiting by remember { mutableStateOf(false) } if (isWaiting) { @@ -269,6 +272,8 @@ fun ChannelScreen( ) } + requestChannelSet?.let { ScannedQrCodeDialog(it, onDismiss = { viewModel.clearRequestChannelUrl() }) } + val listState = rememberLazyListState() LazyColumn(state = listState, contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp)) { item { diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt index 01f24b7d2..4b05764f1 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt @@ -91,6 +91,10 @@ constructor( onError() } + fun clearRequestChannelUrl() { + _requestChannelSet.value = null + } + /** Set the radio config (also updates our saved copy in preferences). */ fun setChannels(channelSet: AppOnlyProtos.ChannelSet) = viewModelScope.launch { getChannelList(channelSet.settingsList, channels.value.settingsList).forEach(::setChannel) From 2accdd7f7720dff373c50037f5f941164ed84bc3 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Fri, 3 Oct 2025 05:16:16 -0500 Subject: [PATCH 4/9] New Crowdin updates (#3303) --- core/strings/src/main/res/values-et-rEE/strings.xml | 6 +++--- core/strings/src/main/res/values-fi-rFI/strings.xml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/strings/src/main/res/values-et-rEE/strings.xml b/core/strings/src/main/res/values-et-rEE/strings.xml index 7a5dd68ec..32e3cb542 100644 --- a/core/strings/src/main/res/values-et-rEE/strings.xml +++ b/core/strings/src/main/res/values-et-rEE/strings.xml @@ -786,8 +786,8 @@ PAX andurite logi ei ole saadaval. WiFi seadmed Sinihamba seadmed - Paired devices - Available devices + Seotud seadmed + Saadaolevad seadmed Ühendatud seadmed Limiit ületatud. Proovi hiljem uuesti. Näita versioon @@ -888,5 +888,5 @@ Filtreeri viimase kuulmise aja järgi: %s %1$d dBm Lingi haldamiseks pole rakendust saadaval. - System Settings + Süsteemi sätted diff --git a/core/strings/src/main/res/values-fi-rFI/strings.xml b/core/strings/src/main/res/values-fi-rFI/strings.xml index a39aafabf..7ca22da47 100644 --- a/core/strings/src/main/res/values-fi-rFI/strings.xml +++ b/core/strings/src/main/res/values-fi-rFI/strings.xml @@ -786,8 +786,8 @@ PAX-laskurien lokitietoja ei ole saatavilla. WiFi-laitteet Bluetooth-laitteet - Paired devices - Available devices + Paritetut laitteet + Käytettävissä olevat laitteet Yhdistetty laite Käyttöraja ylitetty. Yritä myöhemmin uudelleen. Näytä versio From 5d95dca354dc12c952de12c8c80b049ea95dd9ac Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Fri, 3 Oct 2025 06:42:52 -0400 Subject: [PATCH 5/9] 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> --- app/detekt-baseline.xml | 2 + .../java/com/geeksville/mesh/MainActivity.kt | 6 +- .../java/com/geeksville/mesh/model/UIState.kt | 14 +++- .../main/java/com/geeksville/mesh/ui/Main.kt | 6 ++ .../geeksville/mesh/ui/node/NodeListScreen.kt | 6 +- .../mesh/ui/sharing/ContactSharing.kt | 41 +---------- .../mesh/ui/sharing/SharedContactDialog.kt | 72 +++++++++++++++++++ .../mesh/ui/sharing/SharedContactViewModel.kt | 53 ++++++++++++++ core/strings/src/main/res/values/strings.xml | 1 + feature/node/detekt-baseline.xml | 1 - .../feature/node/list/NodeListViewModel.kt | 3 - 11 files changed, 153 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/ui/sharing/SharedContactDialog.kt create mode 100644 app/src/main/java/com/geeksville/mesh/ui/sharing/SharedContactViewModel.kt diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index 4af55d5f5..571bc53f6 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -66,6 +66,7 @@ FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt ForbiddenComment:SafeBluetooth.kt$SafeBluetooth$// TODO: display some kind of UI about restarting BLE LambdaParameterEventTrailing:Channel.kt$onConfirm + LambdaParameterEventTrailing:ContactSharing.kt$onSharedContactRequested LambdaParameterEventTrailing:Message.kt$onClick LambdaParameterEventTrailing:Message.kt$onSendMessage LambdaParameterEventTrailing:MessageList.kt$onReply @@ -178,6 +179,7 @@ ModifierMissing:SecurityConfigItemList.kt$SecurityConfigScreen ModifierMissing:SettingsScreen.kt$SettingsScreen ModifierMissing:Share.kt$ShareScreen + ModifierMissing:SharedContactDialog.kt$SharedContactDialog ModifierMissing:SignalMetrics.kt$SignalMetricsScreen ModifierMissing:TopLevelNavIcon.kt$TopLevelNavIcon ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT) diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index c1ffaaed5..2e32a4a20 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -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") } diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index e15419249..85751f9ff 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -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 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 diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt index 30fe3b5a7..fc2ce0701 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -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() }) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeListScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeListScreen.kt index 7167d9750..ace46d5dd 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeListScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeListScreen.kt @@ -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) }, ) }, diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt index 8d9c8eae0..5a1550d3d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt @@ -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, - 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") diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/SharedContactDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/SharedContactDialog.kt new file mode 100644 index 000000000..8eabdd3ae --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/SharedContactDialog.kt @@ -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 . + */ + +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() + }, + ) +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/SharedContactViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/SharedContactViewModel.kt new file mode 100644 index 000000000..3d0c17c75 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/SharedContactViewModel.kt @@ -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 . + */ + +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> = + nodeRepository + .getNodes() + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList(), + ) + + fun addSharedContact(sharedContact: AdminProtos.SharedContact) = + viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.ImportContact(sharedContact)) } +} diff --git a/core/strings/src/main/res/values/strings.xml b/core/strings/src/main/res/values/strings.xml index a231f5a4d..09a95805a 100644 --- a/core/strings/src/main/res/values/strings.xml +++ b/core/strings/src/main/res/values/strings.xml @@ -214,6 +214,7 @@ Service notifications About This Channel URL is invalid and can not be used + This contact is invalid and can not be added Debug Panel Decoded Payload: Export Logs diff --git a/feature/node/detekt-baseline.xml b/feature/node/detekt-baseline.xml index ac9bafa3a..0d1144994 100644 --- a/feature/node/detekt-baseline.xml +++ b/feature/node/detekt-baseline.xml @@ -14,6 +14,5 @@ ParameterNaming:NodeFilterTextField.kt$onToggleShowIgnored PreviewPublic:NodeItem.kt$NodeInfoPreview PreviewPublic:NodeItem.kt$NodeInfoSimplePreview - TooManyFunctions:NodeListViewModel.kt$NodeListViewModel : ViewModel diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt index d43a5d1c9..e7cc0657b 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt @@ -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 } From 7a2d4c642100a0e39ee8cd1d3499a57950e767af Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Fri, 3 Oct 2025 06:26:25 -0500 Subject: [PATCH 6/9] New Crowdin updates (#3304) --- core/strings/src/main/res/values-ar-rSA/strings.xml | 1 + core/strings/src/main/res/values-b+sr+Latn/strings.xml | 1 + core/strings/src/main/res/values-bg-rBG/strings.xml | 1 + core/strings/src/main/res/values-ca-rES/strings.xml | 1 + core/strings/src/main/res/values-cs-rCZ/strings.xml | 1 + core/strings/src/main/res/values-de-rDE/strings.xml | 1 + core/strings/src/main/res/values-el-rGR/strings.xml | 1 + core/strings/src/main/res/values-es-rES/strings.xml | 1 + core/strings/src/main/res/values-et-rEE/strings.xml | 1 + core/strings/src/main/res/values-fi-rFI/strings.xml | 1 + core/strings/src/main/res/values-fr-rFR/strings.xml | 1 + core/strings/src/main/res/values-ga-rIE/strings.xml | 1 + core/strings/src/main/res/values-gl-rES/strings.xml | 1 + core/strings/src/main/res/values-hr-rHR/strings.xml | 1 + core/strings/src/main/res/values-ht-rHT/strings.xml | 1 + core/strings/src/main/res/values-hu-rHU/strings.xml | 1 + core/strings/src/main/res/values-is-rIS/strings.xml | 1 + core/strings/src/main/res/values-it-rIT/strings.xml | 1 + core/strings/src/main/res/values-iw-rIL/strings.xml | 1 + core/strings/src/main/res/values-ja-rJP/strings.xml | 1 + core/strings/src/main/res/values-ko-rKR/strings.xml | 1 + core/strings/src/main/res/values-lt-rLT/strings.xml | 1 + core/strings/src/main/res/values-nl-rNL/strings.xml | 1 + core/strings/src/main/res/values-no-rNO/strings.xml | 1 + core/strings/src/main/res/values-pl-rPL/strings.xml | 1 + core/strings/src/main/res/values-pt-rBR/strings.xml | 1 + core/strings/src/main/res/values-pt-rPT/strings.xml | 1 + core/strings/src/main/res/values-ro-rRO/strings.xml | 1 + core/strings/src/main/res/values-ru-rRU/strings.xml | 1 + core/strings/src/main/res/values-sk-rSK/strings.xml | 1 + core/strings/src/main/res/values-sl-rSI/strings.xml | 1 + core/strings/src/main/res/values-sq-rAL/strings.xml | 1 + core/strings/src/main/res/values-srp/strings.xml | 1 + core/strings/src/main/res/values-sv-rSE/strings.xml | 1 + core/strings/src/main/res/values-tr-rTR/strings.xml | 1 + core/strings/src/main/res/values-uk-rUA/strings.xml | 1 + core/strings/src/main/res/values-zh-rCN/strings.xml | 1 + core/strings/src/main/res/values-zh-rTW/strings.xml | 1 + 38 files changed, 38 insertions(+) diff --git a/core/strings/src/main/res/values-ar-rSA/strings.xml b/core/strings/src/main/res/values-ar-rSA/strings.xml index 114ea7485..42349aa97 100644 --- a/core/strings/src/main/res/values-ar-rSA/strings.xml +++ b/core/strings/src/main/res/values-ar-rSA/strings.xml @@ -187,6 +187,7 @@ خدمة الإشعارات حول This Channel URL is invalid and can not be used + This contact is invalid and can not be added Debug Panel Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-b+sr+Latn/strings.xml b/core/strings/src/main/res/values-b+sr+Latn/strings.xml index e1b9dc1ab..194b8ef82 100644 --- a/core/strings/src/main/res/values-b+sr+Latn/strings.xml +++ b/core/strings/src/main/res/values-b+sr+Latn/strings.xml @@ -187,6 +187,7 @@ Servisna obaveštenja O nama Ovaj URL kanala je nevažeći i ne može se koristiti. + This contact is invalid and can not be added Panel za otklanjanje grešaka Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-bg-rBG/strings.xml b/core/strings/src/main/res/values-bg-rBG/strings.xml index 6a2e93963..9cc8c7dc3 100644 --- a/core/strings/src/main/res/values-bg-rBG/strings.xml +++ b/core/strings/src/main/res/values-bg-rBG/strings.xml @@ -187,6 +187,7 @@ Сервизни известия Относно URL адресът на този канал е невалиден и не може да се използва + This contact is invalid and can not be added Панел за отстраняване на грешки Decoded Payload: Експортиране на журнали diff --git a/core/strings/src/main/res/values-ca-rES/strings.xml b/core/strings/src/main/res/values-ca-rES/strings.xml index 7cbdd992d..a4efc132a 100644 --- a/core/strings/src/main/res/values-ca-rES/strings.xml +++ b/core/strings/src/main/res/values-ca-rES/strings.xml @@ -187,6 +187,7 @@ Notificacions de servei Sobre La URL d\'aquest canal és invàlida i no es pot fer servir + This contact is invalid and can not be added Panell de depuració Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-cs-rCZ/strings.xml b/core/strings/src/main/res/values-cs-rCZ/strings.xml index 9f4131295..a188cd5f7 100644 --- a/core/strings/src/main/res/values-cs-rCZ/strings.xml +++ b/core/strings/src/main/res/values-cs-rCZ/strings.xml @@ -187,6 +187,7 @@ Servisní upozornění O aplikaci Tato adresa URL kanálu je neplatná a nelze ji použít + This contact is invalid and can not be added Panel pro ladění Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-de-rDE/strings.xml b/core/strings/src/main/res/values-de-rDE/strings.xml index 21dd386b2..e802751ea 100644 --- a/core/strings/src/main/res/values-de-rDE/strings.xml +++ b/core/strings/src/main/res/values-de-rDE/strings.xml @@ -187,6 +187,7 @@ Dienstbenachrichtigungen Über Diese Kanal-URL ist ungültig und kann nicht verwendet werden + This contact is invalid and can not be added Debug-Ausgaben Dekodiertes Payload: Protokolle exportieren diff --git a/core/strings/src/main/res/values-el-rGR/strings.xml b/core/strings/src/main/res/values-el-rGR/strings.xml index 78cd634e3..1e6a4846e 100644 --- a/core/strings/src/main/res/values-el-rGR/strings.xml +++ b/core/strings/src/main/res/values-el-rGR/strings.xml @@ -187,6 +187,7 @@ Ειδοποιήσεις Υπηρεσίας Σχετικά Αυτό το κανάλι URL δεν είναι ορθό και δεν μπορεί να χρησιμοποιηθεί + This contact is invalid and can not be added Πίνακας αποσφαλμάτωσης Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-es-rES/strings.xml b/core/strings/src/main/res/values-es-rES/strings.xml index a8cf9ae6d..ca30ea1e4 100644 --- a/core/strings/src/main/res/values-es-rES/strings.xml +++ b/core/strings/src/main/res/values-es-rES/strings.xml @@ -187,6 +187,7 @@ Notificaciones de servicio Acerca de La URL de este canal no es válida y no puede utilizarse + This contact is invalid and can not be added Panel de depuración Decoded Payload: Exportar registros diff --git a/core/strings/src/main/res/values-et-rEE/strings.xml b/core/strings/src/main/res/values-et-rEE/strings.xml index 32e3cb542..6339475fc 100644 --- a/core/strings/src/main/res/values-et-rEE/strings.xml +++ b/core/strings/src/main/res/values-et-rEE/strings.xml @@ -187,6 +187,7 @@ Teenuse teavitused Teave Kanali URL on kehtetu ja seda ei saa kasutada + This contact is invalid and can not be added Arendaja paneel Dekodeeritud andmed: Salvesta logi diff --git a/core/strings/src/main/res/values-fi-rFI/strings.xml b/core/strings/src/main/res/values-fi-rFI/strings.xml index 7ca22da47..e9b6ea02e 100644 --- a/core/strings/src/main/res/values-fi-rFI/strings.xml +++ b/core/strings/src/main/res/values-fi-rFI/strings.xml @@ -187,6 +187,7 @@ Palveluilmoitukset Tietoja Kanavan URL-osoite on virheellinen, eikä sitä voida käyttää + This contact is invalid and can not be added Vianetsintäpaneeli Dekoodattu data: Vie lokitiedot diff --git a/core/strings/src/main/res/values-fr-rFR/strings.xml b/core/strings/src/main/res/values-fr-rFR/strings.xml index a4ef0dad8..60aaacfe5 100644 --- a/core/strings/src/main/res/values-fr-rFR/strings.xml +++ b/core/strings/src/main/res/values-fr-rFR/strings.xml @@ -187,6 +187,7 @@ Notifications de service A propros Cette URL de canal est invalide et ne peut pas être utilisée + This contact is invalid and can not be added Panneau de débogage Contenu décodé : Exporter les logs diff --git a/core/strings/src/main/res/values-ga-rIE/strings.xml b/core/strings/src/main/res/values-ga-rIE/strings.xml index 5b8e5de45..150ce5699 100644 --- a/core/strings/src/main/res/values-ga-rIE/strings.xml +++ b/core/strings/src/main/res/values-ga-rIE/strings.xml @@ -187,6 +187,7 @@ Fógraí seirbhíse Maidir le Tá an URL Cainéil seo neamhdhleathach agus ní féidir é a úsáid + This contact is invalid and can not be added Painéal Laige Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-gl-rES/strings.xml b/core/strings/src/main/res/values-gl-rES/strings.xml index 22c7d6d1b..4ab6395a2 100644 --- a/core/strings/src/main/res/values-gl-rES/strings.xml +++ b/core/strings/src/main/res/values-gl-rES/strings.xml @@ -187,6 +187,7 @@ Notificacións de servizo Acerca de A ligazón desta canle non é válida e non pode usarse + This contact is invalid and can not be added Panel de depuración Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-hr-rHR/strings.xml b/core/strings/src/main/res/values-hr-rHR/strings.xml index a28d92dc9..74aa98a51 100644 --- a/core/strings/src/main/res/values-hr-rHR/strings.xml +++ b/core/strings/src/main/res/values-hr-rHR/strings.xml @@ -187,6 +187,7 @@ Servisne obavijesti O programu Ovaj URL kanala je nevažeći i ne može se koristiti + This contact is invalid and can not be added Otklanjanje pogrešaka Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-ht-rHT/strings.xml b/core/strings/src/main/res/values-ht-rHT/strings.xml index 67e4e3818..b86fa8a97 100644 --- a/core/strings/src/main/res/values-ht-rHT/strings.xml +++ b/core/strings/src/main/res/values-ht-rHT/strings.xml @@ -187,6 +187,7 @@ Notifikasyon sèvis Sou Kanal URL sa a pa valab e yo pa kapab itilize li + This contact is invalid and can not be added Panno Debug Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-hu-rHU/strings.xml b/core/strings/src/main/res/values-hu-rHU/strings.xml index c4f92eafb..528a14813 100644 --- a/core/strings/src/main/res/values-hu-rHU/strings.xml +++ b/core/strings/src/main/res/values-hu-rHU/strings.xml @@ -187,6 +187,7 @@ Szolgáltatás értesítések A programról Ez a csatorna URL érvénytelen, ezért nem használható. + This contact is invalid and can not be added Hibakereső panel Dekódolt adat: Naplók exportálása diff --git a/core/strings/src/main/res/values-is-rIS/strings.xml b/core/strings/src/main/res/values-is-rIS/strings.xml index 6d66ada36..d88e13b7c 100644 --- a/core/strings/src/main/res/values-is-rIS/strings.xml +++ b/core/strings/src/main/res/values-is-rIS/strings.xml @@ -187,6 +187,7 @@ Tilkynningar um þjónustu Um smáforrit Þetta rásar URL er ógilt og ónothæft + This contact is invalid and can not be added Villuleitarborð Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-it-rIT/strings.xml b/core/strings/src/main/res/values-it-rIT/strings.xml index 16e88245b..bb5afd521 100644 --- a/core/strings/src/main/res/values-it-rIT/strings.xml +++ b/core/strings/src/main/res/values-it-rIT/strings.xml @@ -187,6 +187,7 @@ Notifiche di servizio Informazioni L\'URL di questo Canale non è valida e non può essere usata + This contact is invalid and can not be added Pannello Di Debug Payload decodificato: Esporta i logs diff --git a/core/strings/src/main/res/values-iw-rIL/strings.xml b/core/strings/src/main/res/values-iw-rIL/strings.xml index b3fe9f57c..6e6d50d0b 100644 --- a/core/strings/src/main/res/values-iw-rIL/strings.xml +++ b/core/strings/src/main/res/values-iw-rIL/strings.xml @@ -187,6 +187,7 @@ התראות שירות אודות כתובת ערוץ זה אינו תקין ולא ניתן לעשות בו שימוש + This contact is invalid and can not be added פאנל דיבאג Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-ja-rJP/strings.xml b/core/strings/src/main/res/values-ja-rJP/strings.xml index 03149f898..a1fe0a7e3 100644 --- a/core/strings/src/main/res/values-ja-rJP/strings.xml +++ b/core/strings/src/main/res/values-ja-rJP/strings.xml @@ -188,6 +188,7 @@ 通知サービス 概要 このチャンネルURLは無効なため使用できません。 + This contact is invalid and can not be added デバッグ Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-ko-rKR/strings.xml b/core/strings/src/main/res/values-ko-rKR/strings.xml index 44e734b48..6c2aaa3b1 100644 --- a/core/strings/src/main/res/values-ko-rKR/strings.xml +++ b/core/strings/src/main/res/values-ko-rKR/strings.xml @@ -187,6 +187,7 @@ 서비스 알림 앱에 대하여 이 채널 URL은 유효하지 않으며 사용할 수 없습니다. + This contact is invalid and can not be added 디버그 패널 Decoded Payload: 로그 내보내기 diff --git a/core/strings/src/main/res/values-lt-rLT/strings.xml b/core/strings/src/main/res/values-lt-rLT/strings.xml index 65d4fa2e0..8934de35a 100644 --- a/core/strings/src/main/res/values-lt-rLT/strings.xml +++ b/core/strings/src/main/res/values-lt-rLT/strings.xml @@ -187,6 +187,7 @@ Paslaugos pranešimai Apie Šio kanalo URL yra neteisingas ir negali būti naudojamas + This contact is invalid and can not be added Derinimo skydelis Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-nl-rNL/strings.xml b/core/strings/src/main/res/values-nl-rNL/strings.xml index 60ac3c037..bd35c7525 100644 --- a/core/strings/src/main/res/values-nl-rNL/strings.xml +++ b/core/strings/src/main/res/values-nl-rNL/strings.xml @@ -187,6 +187,7 @@ Servicemeldingen Over Deze Kanaal URL is ongeldig en kan niet worden gebruikt + This contact is invalid and can not be added Debug-paneel Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-no-rNO/strings.xml b/core/strings/src/main/res/values-no-rNO/strings.xml index 7cfc3539b..16f6985c2 100644 --- a/core/strings/src/main/res/values-no-rNO/strings.xml +++ b/core/strings/src/main/res/values-no-rNO/strings.xml @@ -187,6 +187,7 @@ Tjeneste meldinger Om Denne kanall URL er ugyldig og kan ikke benyttes + This contact is invalid and can not be added Feilsøkningspanel Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-pl-rPL/strings.xml b/core/strings/src/main/res/values-pl-rPL/strings.xml index 133540c64..63603d3c9 100644 --- a/core/strings/src/main/res/values-pl-rPL/strings.xml +++ b/core/strings/src/main/res/values-pl-rPL/strings.xml @@ -187,6 +187,7 @@ Powiadomienia o usługach O aplikacji Ten adres URL kanału jest nieprawidłowy i nie można go użyć + This contact is invalid and can not be added Panel debugowania Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-pt-rBR/strings.xml b/core/strings/src/main/res/values-pt-rBR/strings.xml index d97e74a68..5b7c6d024 100644 --- a/core/strings/src/main/res/values-pt-rBR/strings.xml +++ b/core/strings/src/main/res/values-pt-rBR/strings.xml @@ -187,6 +187,7 @@ Notificações de serviço Sobre Este link de canal é inválido e não pode ser usado + This contact is invalid and can not be added Painel de depuração Pacote Decodificado: Exportar Logs diff --git a/core/strings/src/main/res/values-pt-rPT/strings.xml b/core/strings/src/main/res/values-pt-rPT/strings.xml index 9b46f7b72..74316f20a 100644 --- a/core/strings/src/main/res/values-pt-rPT/strings.xml +++ b/core/strings/src/main/res/values-pt-rPT/strings.xml @@ -187,6 +187,7 @@ Notificações de serviço Sobre O Link Deste Canal é inválido e não pode ser usado + This contact is invalid and can not be added Painel de depuração Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-ro-rRO/strings.xml b/core/strings/src/main/res/values-ro-rRO/strings.xml index a396602d9..3624f4040 100644 --- a/core/strings/src/main/res/values-ro-rRO/strings.xml +++ b/core/strings/src/main/res/values-ro-rRO/strings.xml @@ -187,6 +187,7 @@ Notificările serviciului Despre Acest URL de canal este invalid și nu poate fi folosit + This contact is invalid and can not be added Panou debug Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-ru-rRU/strings.xml b/core/strings/src/main/res/values-ru-rRU/strings.xml index 4795f79bb..9c5a77c20 100644 --- a/core/strings/src/main/res/values-ru-rRU/strings.xml +++ b/core/strings/src/main/res/values-ru-rRU/strings.xml @@ -187,6 +187,7 @@ Служебные уведомления О приложении Этот URL-адрес канала недействителен и не может быть использован + This contact is invalid and can not be added Панель отладки Декодированная нагрузка: Экспортировать логи diff --git a/core/strings/src/main/res/values-sk-rSK/strings.xml b/core/strings/src/main/res/values-sk-rSK/strings.xml index 68856abaf..3979e4271 100644 --- a/core/strings/src/main/res/values-sk-rSK/strings.xml +++ b/core/strings/src/main/res/values-sk-rSK/strings.xml @@ -187,6 +187,7 @@ Notifikácie zo služby O aplikácii URL adresa tohoto kanála nie je platná a nedá sa použiť + This contact is invalid and can not be added Debug okno Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-sl-rSI/strings.xml b/core/strings/src/main/res/values-sl-rSI/strings.xml index 4f5540f8e..993468940 100644 --- a/core/strings/src/main/res/values-sl-rSI/strings.xml +++ b/core/strings/src/main/res/values-sl-rSI/strings.xml @@ -187,6 +187,7 @@ Obvestila storitve O programu Neveljaven kanal + This contact is invalid and can not be added Plošča za odpravljanje napak Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-sq-rAL/strings.xml b/core/strings/src/main/res/values-sq-rAL/strings.xml index b93ee1020..393f19430 100644 --- a/core/strings/src/main/res/values-sq-rAL/strings.xml +++ b/core/strings/src/main/res/values-sq-rAL/strings.xml @@ -187,6 +187,7 @@ Njoftime shërbimi Rreth Ky URL kanal është i pavlefshëm dhe nuk mund të përdoret + This contact is invalid and can not be added Paneli i debug-ut Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-srp/strings.xml b/core/strings/src/main/res/values-srp/strings.xml index b5534392e..132e787a2 100644 --- a/core/strings/src/main/res/values-srp/strings.xml +++ b/core/strings/src/main/res/values-srp/strings.xml @@ -187,6 +187,7 @@ Обавештења о услугама О Ова URL адреса канала је неважећа и не може се користити + This contact is invalid and can not be added Панел за отклањање грешака Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-sv-rSE/strings.xml b/core/strings/src/main/res/values-sv-rSE/strings.xml index aa78dc70e..eac530440 100644 --- a/core/strings/src/main/res/values-sv-rSE/strings.xml +++ b/core/strings/src/main/res/values-sv-rSE/strings.xml @@ -187,6 +187,7 @@ Tjänsteaviseringar Om Denna kanal-URL är ogiltig och kan inte användas + This contact is invalid and can not be added Felsökningspanel Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-tr-rTR/strings.xml b/core/strings/src/main/res/values-tr-rTR/strings.xml index fc28b11bf..0df19b417 100644 --- a/core/strings/src/main/res/values-tr-rTR/strings.xml +++ b/core/strings/src/main/res/values-tr-rTR/strings.xml @@ -187,6 +187,7 @@ Servis bildirimleri Hakkında Bu Kanal URL\' si geçersiz ve kullanılamaz + This contact is invalid and can not be added Hata Ayıklama Paneli Decoded Payload: Export Logs diff --git a/core/strings/src/main/res/values-uk-rUA/strings.xml b/core/strings/src/main/res/values-uk-rUA/strings.xml index d80268278..b22de4874 100644 --- a/core/strings/src/main/res/values-uk-rUA/strings.xml +++ b/core/strings/src/main/res/values-uk-rUA/strings.xml @@ -187,6 +187,7 @@ Сервісні сповіщення Про URL-адреса цього каналу недійсна та не може бути використана + This contact is invalid and can not be added Панель налагодження Decoded Payload: Експортувати журнали diff --git a/core/strings/src/main/res/values-zh-rCN/strings.xml b/core/strings/src/main/res/values-zh-rCN/strings.xml index 988bdfca8..6383be217 100644 --- a/core/strings/src/main/res/values-zh-rCN/strings.xml +++ b/core/strings/src/main/res/values-zh-rCN/strings.xml @@ -187,6 +187,7 @@ 服务通知 关于 此频道 URL 无效,无法使用 + This contact is invalid and can not be added 调试面板 解码Payload: 导出程序日志 diff --git a/core/strings/src/main/res/values-zh-rTW/strings.xml b/core/strings/src/main/res/values-zh-rTW/strings.xml index 21da6cf59..6efa62636 100644 --- a/core/strings/src/main/res/values-zh-rTW/strings.xml +++ b/core/strings/src/main/res/values-zh-rTW/strings.xml @@ -187,6 +187,7 @@ 服務通知 關於 此頻道 URL 無效,無法使用 + This contact is invalid and can not be added 除錯面板 解析封包: 匯出日誌 From 87f7ea3f47d3e809ca52b70403740e13f48a8107 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:11:55 +0000 Subject: [PATCH 7/9] chore(deps): update core/proto/src/main/proto digest to c1e31a9 (#3305) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- core/proto/src/main/proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/proto/src/main/proto b/core/proto/src/main/proto index 60c3e6600..c1e31a965 160000 --- a/core/proto/src/main/proto +++ b/core/proto/src/main/proto @@ -1 +1 @@ -Subproject commit 60c3e6600a2f4e6f49e45aeb47aafd8291a0015c +Subproject commit c1e31a9655e9920a8b5b8eccdf7c69ef1ae42a49 From 7bc9469df5f327919768552cd3efd8c6e896dc2b Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Fri, 3 Oct 2025 09:33:09 -0500 Subject: [PATCH 8/9] feat(ci): overhaul release workflow for hotfixes and promotions (#3307) --- .github/workflows/create-internal-release.yml | 139 ++++++ .github/workflows/promote-release.yml | 191 ++++++++ .github/workflows/release.yml | 452 ++++++++++++++---- RELEASE_PROCESS.md | 25 + fastlane/Fastfile | 70 ++- 5 files changed, 760 insertions(+), 117 deletions(-) create mode 100644 .github/workflows/create-internal-release.yml create mode 100644 .github/workflows/promote-release.yml diff --git a/.github/workflows/create-internal-release.yml b/.github/workflows/create-internal-release.yml new file mode 100644 index 000000000..68be90ae5 --- /dev/null +++ b/.github/workflows/create-internal-release.yml @@ -0,0 +1,139 @@ +name: Create Internal Release Tag + +on: + workflow_dispatch: + inputs: + release_type: + description: "Type of release (auto|patch|minor|major|hotfix)" + required: true + default: auto + type: choice + options: [auto, patch, minor, major, hotfix] + hotfix_base_version: + description: "Base version for hotfix (e.g. 2.5.0) required if release_type=hotfix" + required: false + dry_run: + description: "If true, calculate but do not push tag" + required: false + default: "false" + +permissions: + contents: write + +jobs: + create-internal-tag: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Determine Latest Base Version + id: latest + run: | + set -euo pipefail + # List base tags (exclude track/hotfix suffixes) + BASE_TAGS=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | head -n1 || true) + echo "Found latest base tag: $BASE_TAGS" + echo "latest_base_tag=$BASE_TAGS" >> $GITHUB_OUTPUT + + - name: Compute Next Version (auto/patch/minor/major) + id: compute + if: ${{ inputs.release_type != 'hotfix' }} + run: | + set -euo pipefail + RTYPE='${{ inputs.release_type }}' + LAST='${{ steps.latest.outputs.latest_base_tag }}' + if [ -z "$LAST" ]; then + BASE_MAJOR=0; BASE_MINOR=0; BASE_PATCH=0 + else + V=${LAST#v} + IFS='.' read -r BASE_MAJOR BASE_MINOR BASE_PATCH <<< "$V" + fi + if [ "$RTYPE" = 'auto' ]; then + echo "Determining bump type from commits since $LAST..." + RANGE="$LAST..HEAD" + [ -z "$LAST" ] && RANGE="HEAD" # first release + LOG=$(git log --format=%s $RANGE || true) + BUMP="patch" + if echo "$LOG" | grep -Eiq 'BREAKING CHANGE'; then BUMP=major; fi + if echo "$LOG" | grep -Eiq '^[a-zA-Z]+!:'; then BUMP=major; fi + if [ "$BUMP" != major ] && echo "$LOG" | grep -Eiq '^feat(\(|:)' ; then BUMP=minor; fi + RTYPE=$BUMP + echo "Auto-detected bump: $RTYPE" + fi + case "$RTYPE" in + major) + NEW_MAJOR=$((BASE_MAJOR+1)); NEW_MINOR=0; NEW_PATCH=0;; + minor) + NEW_MAJOR=$BASE_MAJOR; NEW_MINOR=$((BASE_MINOR+1)); NEW_PATCH=0;; + patch) + NEW_MAJOR=$BASE_MAJOR; NEW_MINOR=$BASE_MINOR; NEW_PATCH=$((BASE_PATCH+1));; + *) echo "Unsupported release_type for this step: $RTYPE"; exit 1;; + esac + NEW_VERSION="${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}" + echo "base_version=$NEW_VERSION" >> $GITHUB_OUTPUT + + - name: Compute Hotfix Version + id: hotfix + if: ${{ inputs.release_type == 'hotfix' }} + run: | + set -euo pipefail + BASE='${{ inputs.hotfix_base_version }}' + if [ -z "$BASE" ]; then + echo "hotfix_base_version required for hotfix release_type" >&2 + exit 1 + fi + if ! git tag --list | grep -q "^v$BASE$"; then + echo "Base version tag v$BASE not found (production tag required)." >&2 + exit 1 + fi + EXISTING=$(git tag --list "v${BASE}-hotfix*" | sed -E 's/^v[0-9]+\.[0-9]+\.[0-9]+-hotfix([0-9]+).*$/\1/' | sort -n | tail -1 || true) + if [ -z "$EXISTING" ]; then NEXT=1; else NEXT=$((EXISTING+1)); fi + HOTFIX_VERSION="${BASE}-hotfix${NEXT}" + echo "hotfix_version=$HOTFIX_VERSION" >> $GITHUB_OUTPUT + + - name: Decide Internal Tag + id: tag + run: | + set -euo pipefail + if [ '${{ inputs.release_type }}' = 'hotfix' ]; then + BASE='${{ steps.hotfix.outputs.hotfix_version }}' + else + BASE='${{ steps.compute.outputs.base_version }}' + fi + INTERNAL_TAG="v${BASE}-internal.1" + if git tag --list | grep -q "^${INTERNAL_TAG}$"; then + echo "Tag ${INTERNAL_TAG} already exists." >&2 + exit 1 + fi + echo "internal_tag=$INTERNAL_TAG" >> $GITHUB_OUTPUT + + - name: Dry Run Preview + if: ${{ inputs.dry_run == 'true' }} + run: | + echo "DRY RUN: Would create tag ${{ steps.tag.outputs.internal_tag }} pointing to $(git rev-parse HEAD)" + git log -5 --oneline + + - name: Create and Push Tag + if: ${{ inputs.dry_run != 'true' }} + run: | + TAG='${{ steps.tag.outputs.internal_tag }}' + MSG="Initial internal build for ${TAG}" + git tag -a "$TAG" -m "$MSG" + git push origin "$TAG" + echo "Created and pushed $TAG" + + - name: Output Summary + run: | + echo "### Internal Tag Created" >> $GITHUB_STEP_SUMMARY + echo "Tag: ${{ steps.tag.outputs.internal_tag }}" >> $GITHUB_STEP_SUMMARY + echo "Release Type: ${{ inputs.release_type }}" >> $GITHUB_STEP_SUMMARY + if [ '${{ inputs.release_type }}' = 'hotfix' ]; then + echo "Base Hotfix Series: ${{ steps.hotfix.outputs.hotfix_version }}" >> $GITHUB_STEP_SUMMARY + else + echo "Base Version: ${{ steps.compute.outputs.base_version }}" >> $GITHUB_STEP_SUMMARY + fi + echo "Dry Run: ${{ inputs.dry_run }}" >> $GITHUB_STEP_SUMMARY + diff --git a/.github/workflows/promote-release.yml b/.github/workflows/promote-release.yml new file mode 100644 index 000000000..04d42bb25 --- /dev/null +++ b/.github/workflows/promote-release.yml @@ -0,0 +1,191 @@ +name: Promote Release + +on: + workflow_dispatch: + inputs: + target_stage: + description: "Stage to promote to (auto|closed|open|production)" + required: true + default: auto + type: choice + options: [auto, closed, open, production] + base_version: + description: "Explicit base version (e.g. 2.5.0 or 2.5.0-hotfix1). If omitted, latest internal tag base is used." + required: false + allow_skip: + description: "Allow skipping intermediate stages (e.g. internal->production)" + required: false + default: "false" + dry_run: + description: "If true, only compute next tag; don't push" + required: false + default: "false" + +permissions: + contents: write + +jobs: + promote: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Determine Base Version + id: base + run: | + set -euo pipefail + INPUT_BASE='${{ inputs.base_version }}' + if [ -n "$INPUT_BASE" ]; then + # Validate an internal tag exists for provided base + if ! git tag --list | grep -q "^v${INPUT_BASE}-internal\."; then + echo "No internal tag found for base version v${INPUT_BASE}." >&2 + exit 1 + fi + BASE_VERSION="$INPUT_BASE" + else + LATEST_INTERNAL_TAG=$(git tag --list 'v*-internal.*' --sort=-taggerdate | head -n1 || true) + if [ -z "$LATEST_INTERNAL_TAG" ]; then + echo "No internal tags found; nothing to promote." >&2 + exit 1 + fi + # Strip leading v and suffix -internal.N + BASE_VERSION=$(echo "$LATEST_INTERNAL_TAG" | sed -E 's/^v(.*)-internal\.[0-9]+$/\1/') + fi + echo "Base version: $BASE_VERSION" + echo "base_version=$BASE_VERSION" >> $GITHUB_OUTPUT + + - name: Gather Existing Stage Tags + id: scan + run: | + set -euo pipefail + BASE='${{ steps.base.outputs.base_version }}' + INTERNAL_TAGS=$(git tag --list "v${BASE}-internal.*" | sort -V || true) + CLOSED_TAGS=$(git tag --list "v${BASE}-closed.*" | sort -V || true) + OPEN_TAGS=$(git tag --list "v${BASE}-open.*" | sort -V || true) + PROD_TAG=$(git tag --list "v${BASE}" || true) + echo "internal_tags<> $GITHUB_OUTPUT + echo "$INTERNAL_TAGS" >> $GITHUB_OUTPUT + echo EOF >> $GITHUB_OUTPUT + echo "closed_tags<> $GITHUB_OUTPUT + echo "$CLOSED_TAGS" >> $GITHUB_OUTPUT + echo EOF >> $GITHUB_OUTPUT + echo "open_tags<> $GITHUB_OUTPUT + echo "$OPEN_TAGS" >> $GITHUB_OUTPUT + echo EOF >> $GITHUB_OUTPUT + if [ -n "$PROD_TAG" ]; then echo "production_present=true" >> $GITHUB_OUTPUT; else echo "production_present=false" >> $GITHUB_OUTPUT; fi + if [ -z "$INTERNAL_TAGS" ]; then + echo "No internal tags found for base version $BASE." >&2 + exit 1 + fi + + - name: Determine Current Stage + id: current + run: | + set -euo pipefail + PROD='${{ steps.scan.outputs.production_present }}' + CLOSED='${{ steps.scan.outputs.closed_tags }}' + OPEN='${{ steps.scan.outputs.open_tags }}' + if [ "$PROD" = 'true' ]; then CUR=production + elif [ -n "$OPEN" ]; then CUR=open + elif [ -n "$CLOSED" ]; then CUR=closed + else CUR=internal; fi + echo "Current highest stage: $CUR" + echo "current_stage=$CUR" >> $GITHUB_OUTPUT + + - name: Decide Target Stage + id: decide + run: | + set -euo pipefail + REQ='${{ inputs.target_stage }}' + CUR='${{ steps.current.outputs.current_stage }}' + ALLOW_SKIP='${{ inputs.allow_skip }}' + order=(internal closed open production) + # helper to get index + idx() { local i=0; for s in "${order[@]}"; do [ "$s" = "$1" ] && echo $i && return; i=$((i+1)); done; echo -1; } + if [ "$REQ" = auto ]; then + CUR_IDX=$(idx "$CUR") + TARGET_IDX=$((CUR_IDX+1)) + TARGET_STAGE=${order[$TARGET_IDX]:-} + if [ -z "$TARGET_STAGE" ]; then + echo "Already at production; nothing to promote." >&2 + exit 1 + fi + else + TARGET_STAGE=$REQ + CUR_IDX=$(idx "$CUR") + REQ_IDX=$(idx "$TARGET_STAGE") + if [ $REQ_IDX -le $CUR_IDX ]; then + echo "Requested stage $TARGET_STAGE is not ahead of current stage $CUR." >&2 + exit 1 + fi + if [ "$ALLOW_SKIP" != 'true' ] && [ $((CUR_IDX+1)) -ne $REQ_IDX ]; then + echo "Skipping stages not allowed (current=$CUR, requested=$TARGET_STAGE). Enable allow_skip to override." >&2 + exit 1 + fi + fi + echo "Target stage: $TARGET_STAGE" + echo "target_stage=$TARGET_STAGE" >> $GITHUB_OUTPUT + + - name: Compute New Tag + id: tag + run: | + set -euo pipefail + BASE='${{ steps.base.outputs.base_version }}' + TARGET='${{ steps.decide.outputs.target_stage }}' + if [ "$TARGET" = production ]; then + NEW_TAG="v${BASE}" + if git tag --list | grep -q "^${NEW_TAG}$"; then + echo "Production tag ${NEW_TAG} already exists." >&2 + exit 1 + fi + else + EXISTING=$(git tag --list "v${BASE}-${TARGET}.*" | sed -E "s/^v.*-${TARGET}\.([0-9]+)$/\1/" | sort -n | tail -1 || true) + if [ -z "$EXISTING" ]; then NEXT=1; else NEXT=$((EXISTING+1)); fi + NEW_TAG="v${BASE}-${TARGET}.${NEXT}" + fi + echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT + echo "Will create tag: $NEW_TAG" + + - name: Resolve Commit to Tag (latest internal for base) + id: commit + run: | + set -euo pipefail + BASE='${{ steps.base.outputs.base_version }}' + LATEST_INTERNAL=$(git tag --list "v${BASE}-internal.*" --sort=-version:refname | head -n1) + if [ -z "$LATEST_INTERNAL" ]; then + echo "No internal tag found for base $BASE (unexpected)." >&2 + exit 1 + fi + COMMIT=$(git rev-list -n1 "$LATEST_INTERNAL") + echo "commit_sha=$COMMIT" >> $GITHUB_OUTPUT + echo "Using commit $COMMIT from $LATEST_INTERNAL" + + - name: Dry Run Summary + if: ${{ inputs.dry_run == 'true' }} + run: | + echo "DRY RUN: Would tag commit ${{ steps.commit.outputs.commit_sha }} with ${{ steps.tag.outputs.new_tag }}" + echo "Current stage: ${{ steps.current.outputs.current_stage }} -> Target: ${{ steps.decide.outputs.target_stage }}" + git log -1 --oneline ${{ steps.commit.outputs.commit_sha }} + + - name: Create & Push Tag + if: ${{ inputs.dry_run != 'true' }} + run: | + TAG='${{ steps.tag.outputs.new_tag }}' + COMMIT='${{ steps.commit.outputs.commit_sha }}' + MSG="Promote ${TAG} from ${{ steps.current.outputs.current_stage }} to ${{ steps.decide.outputs.target_stage }}" + git tag -a "$TAG" "$COMMIT" -m "$MSG" + git push origin "$TAG" + echo "Created and pushed $TAG" + + - name: Promotion Summary + run: | + echo "### Promotion Tag Created" >> $GITHUB_STEP_SUMMARY + echo "Base Version: ${{ steps.base.outputs.base_version }}" >> $GITHUB_STEP_SUMMARY + echo "Current Stage: ${{ steps.current.outputs.current_stage }}" >> $GITHUB_STEP_SUMMARY + echo "Target Stage: ${{ steps.decide.outputs.target_stage }}" >> $GITHUB_STEP_SUMMARY + echo "New Tag: ${{ steps.tag.outputs.new_tag }}" >> $GITHUB_STEP_SUMMARY + echo "Dry Run: ${{ inputs.dry_run }}" >> $GITHUB_STEP_SUMMARY + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 06d052739..b3606206e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,16 @@ name: Make Release on: workflow_dispatch: + inputs: + dry_run: + description: "If true, simulate the release without building, uploading, promoting, or creating a GitHub release" + required: false + default: "false" + type: choice + options: ["false", "true"] + pr_number: + description: "Optional PR number to comment on with dry-run readiness summary" + required: false push: tags: - 'v*' @@ -21,7 +31,10 @@ jobs: runs-on: ubuntu-latest outputs: APP_VERSION_NAME: ${{ steps.get_version_name.outputs.APP_VERSION_NAME }} - APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }} + FINAL_VERSION_CODE: ${{ steps.final_version_code.outputs.FINAL_VERSION_CODE }} + HOTFIX_PATCH: ${{ steps.is_hotfix_patch.outputs.hotfix_patch }} + BASE_TAG: ${{ steps.get_base_tag.outputs.BASE_TAG }} + FULL_TAG: ${{ steps.get_full_tag.outputs.FULL_TAG }} steps: - name: Checkout code uses: actions/checkout@v5 @@ -45,6 +58,17 @@ jobs: id: get_version_name run: echo "APP_VERSION_NAME=$(echo ${GITHUB_REF_NAME#v} | sed 's/-.*//')" >> $GITHUB_OUTPUT + - name: Get Full Tag + id: get_full_tag + run: echo "FULL_TAG=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT + + - name: Get Base Tag (for release/artifact naming) + id: get_base_tag + run: | + # Remove track/iteration suffix (e.g., -internal.1, -closed.1, -open.1) + BASE_TAG=$(echo ${GITHUB_REF_NAME} | sed 's/\(-internal\.[0-9]\+\|-closed\.[0-9]\+\|-open\.[0-9]\+\)$//') + echo "BASE_TAG=$BASE_TAG" >> $GITHUB_OUTPUT + - name: Extract VERSION_CODE_OFFSET from config.properties id: get_version_code_offset run: | @@ -61,9 +85,89 @@ jobs: shell: bash # This matches the reproducible versionCode strategy: versionCode = git commit count + offset - release-google: + - name: Check if Hotfix or Patch + id: is_hotfix_patch + run: | + TAG_LOWER=$(echo "${GITHUB_REF_NAME}" | tr '[:upper:]' '[:lower:]') + if [[ "$TAG_LOWER" == *"-hotfix"* || "$TAG_LOWER" == *"-patch"* ]]; then + echo "hotfix_patch=true" >> $GITHUB_OUTPUT + else + echo "hotfix_patch=false" >> $GITHUB_OUTPUT + fi + + - name: Download Version Code Artifact (if exists) + id: try_download_version_code + continue-on-error: true + uses: actions/download-artifact@v5 + with: + name: version-code + path: . + + - name: Generate and Store Version Code (first build for regular release) + if: steps.is_hotfix_patch.outputs.hotfix_patch == 'false' && steps.try_download_version_code.outcome != 'success' + id: generate_and_store_version_code + run: | + VERSION_CODE=${{ steps.calculate_version_code.outputs.versionCode }} + echo "versionCode=$VERSION_CODE" >> $GITHUB_OUTPUT + echo "$VERSION_CODE" > version_code.txt + + - name: Upload Version Code Artifact (if generated) + if: | + (steps.is_hotfix_patch.outputs.hotfix_patch == 'true') || + (steps.is_hotfix_patch.outputs.hotfix_patch == 'false' && steps.generate_and_store_version_code.conclusion == 'success') + uses: actions/upload-artifact@v4 + with: + name: version-code + path: version_code.txt + + - name: Set Version Code from Artifact (if exists) + if: steps.try_download_version_code.outcome == 'success' + id: set_version_code_from_artifact + run: | + VERSION_CODE=$(cat version_code.txt) + echo "versionCode=$VERSION_CODE" >> $GITHUB_OUTPUT + + - name: Set Final Version Code Output + id: final_version_code + run: | + if [ -f version_code.txt ]; then + FV=$(cat version_code.txt) + else + FV=${{ steps.calculate_version_code.outputs.versionCode }} + fi + echo "FINAL_VERSION_CODE=$FV" >> $GITHUB_OUTPUT + + check-internal-release: runs-on: ubuntu-latest needs: prepare-build-info + outputs: + exists: ${{ steps.check_release.outputs.exists }} + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + - name: Check for existing GitHub release + id: check_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BASE_TAG=${{ needs.prepare-build-info.outputs.BASE_TAG }} + COMMIT_SHA=$(git rev-parse HEAD) + EXISTING_RELEASE=$(gh release list --limit 100 --json tagName,targetCommitish | jq -r --arg BASE_TAG "$BASE_TAG" --arg COMMIT_SHA "$COMMIT_SHA" '.[] | select(.tagName == $BASE_TAG and .targetCommitish.oid == $COMMIT_SHA)') + if [ -n "$EXISTING_RELEASE" ]; then + echo "An existing release with tag '${BASE_TAG}' was found for this commit." + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "No existing release found for this commit." + echo "exists=false" >> $GITHUB_OUTPUT + fi + + release-google: + runs-on: ubuntu-latest + needs: [prepare-build-info, check-internal-release] + outputs: + INTERNAL_VERSION_CODE: ${{ steps.resolve_internal_version_code.outputs.INTERNAL_VERSION_CODE }} steps: - name: Checkout code uses: actions/checkout@v5 @@ -76,7 +180,6 @@ jobs: java-version: '21' distribution: 'jetbrains' - name: Setup Gradle - if: contains(github.ref_name, '-internal') uses: gradle/actions/setup-gradle@v5 with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} @@ -110,121 +213,258 @@ jobs: ruby-version: '3.2' bundler-cache: true - - name: Determine Fastlane Lane - id: fastlane_lane + - name: Dry Run Sanity Version Code Check + if: github.event.inputs.dry_run == 'true' + id: dry_run_sanity run: | - TAG_NAME="${{ github.ref_name }}" - if [[ "$TAG_NAME" == *"-internal"* ]]; then - echo "lane=internal" >> $GITHUB_OUTPUT - elif [[ "$TAG_NAME" == *"-closed"* ]]; then - echo "lane=closed" >> $GITHUB_OUTPUT - elif [[ "$TAG_NAME" == *"-open"* ]]; then - echo "lane=open" >> $GITHUB_OUTPUT + set -euo pipefail + echo "Performing version code sanity check (dry run)..." + # Query highest existing version code across tracks + bundle exec fastlane get_highest_version_code || true + HIGHEST=$(cat highest_version_code.txt 2>/dev/null || echo 0) + HOTFIX='${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}' + if [ "$HOTFIX" = "true" ]; then + PLANNED=$((HIGHEST + 1)) + echo "Hotfix planned versionCode: $PLANNED (highest existing: $HIGHEST)"; + STATUS=ok else - echo "lane=production" >> $GITHUB_OUTPUT + # Regular base release planned version code (commit count + offset or reused artifact) + PLANNED='${{ needs.prepare-build-info.outputs.FINAL_VERSION_CODE }}' + if [ -z "$PLANNED" ]; then PLANNED=0; fi + if [ "$PLANNED" -le "$HIGHEST" ]; then + echo "ERROR: Planned versionCode $PLANNED is not greater than highest existing $HIGHEST. Adjust VERSION_CODE_OFFSET or convert to hotfix." >&2 + STATUS=fail + else + echo "Planned versionCode $PLANNED is greater than existing $HIGHEST: OK"; + STATUS=ok + fi + fi + echo "sanity_status=$STATUS" >> $GITHUB_OUTPUT + echo "sanity_highest=$HIGHEST" >> $GITHUB_OUTPUT + echo "sanity_planned=$PLANNED" >> $GITHUB_OUTPUT + # Promotion policy validation (if this is a promotion tag in dry run) + TAG='${{ github.ref_name }}' + if echo "$TAG" | grep -Eq '-(closed|open)$' || [[ "$TAG" != *"-internal"* && "$TAG" != *"-closed"* && "$TAG" != *"-open"* && "$TAG" == v* ]]; then + echo "Checking promotion policy (dry run)..." + if ! bundle exec fastlane get_internal_track_version_code; then + echo "ERROR: Promotion attempted but no internal artifact present." >&2 + echo "promotion_status=fail" >> $GITHUB_OUTPUT + [ "$STATUS" = ok ] || true + STATUS=fail + else + echo "Internal artifact present for promotion."; + echo "promotion_status=ok" >> $GITHUB_OUTPUT + fi + else + echo "Not a promotion tag (internal build)."; + echo "promotion_status=na" >> $GITHUB_OUTPUT + fi + if [ "$STATUS" = fail ]; then + echo "Dry run sanity check failed." >&2 + exit 1 fi - - name: Build and Deploy Google Play Tracks with Fastlane - env: - VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }} - VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }} - run: bundle exec fastlane ${{ steps.fastlane_lane.outputs.lane }} - - - name: Upload Google AAB artifact - if: contains(github.ref_name, '-internal') - uses: actions/upload-artifact@v4 - with: - name: google-aab - path: app/build/outputs/bundle/googleRelease/app-google-release.aab - retention-days: 1 - - - name: Upload Google APK artifact - if: contains(github.ref_name, '-internal') - uses: actions/upload-artifact@v4 - with: - name: google-apk - path: app/build/outputs/apk/google/release/app-google-release.apk - retention-days: 1 - - - name: Attest Google artifacts provenance - if: contains(github.ref_name, '-internal') - uses: actions/attest-build-provenance@v3 - with: - subject-path: | - app/build/outputs/bundle/googleRelease/app-google-release.aab - app/build/outputs/apk/google/release/app-google-release.apk - - release-fdroid: - if: contains(github.ref_name, '-internal') - runs-on: ubuntu-latest - needs: prepare-build-info - steps: - - name: Checkout code - uses: actions/checkout@v5 - with: - fetch-depth: 0 - submodules: 'recursive' - - name: Set up JDK 21 - uses: actions/setup-java@v5 - with: - java-version: '21' - distribution: 'jetbrains' - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 - with: - cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - build-scan-publish: true - build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service' - build-scan-terms-of-use-agree: 'yes' - - - name: Load secrets - env: - KEYSTORE: ${{ secrets.KEYSTORE }} - KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }} - KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} + - name: Resolve Version Code For Internal Build + if: needs.check-internal-release.outputs.exists == 'false' + id: resolve_internal_version_code run: | - echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME - echo "$KEYSTORE_PROPERTIES" > ./keystore.properties + if [ "${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}" = "true" ]; then + echo "Hotfix/Patch detected; querying Google Play for highest version code..." + bundle exec fastlane get_highest_version_code + CODE=$(cat highest_version_code.txt || echo 0) + NEXT_CODE=$((CODE + 1)) + # Race mitigation: re-query to ensure no concurrent allocation + bundle exec fastlane get_highest_version_code + NEW_HIGHEST=$(cat highest_version_code.txt || echo 0) + if [ "$NEW_HIGHEST" -ge "$NEXT_CODE" ]; then + echo "Detected race: highest changed from $CODE to $NEW_HIGHEST; bumping again."; + NEXT_CODE=$((NEW_HIGHEST + 1)) + fi + echo "Using hotfix version code: $NEXT_CODE (previous highest final: $NEW_HIGHEST)" + echo "INTERNAL_VERSION_CODE=$NEXT_CODE" >> $GITHUB_OUTPUT + echo "VERSION_CODE=$NEXT_CODE" >> $GITHUB_ENV + else + BASE_CODE=${{ needs.prepare-build-info.outputs.FINAL_VERSION_CODE }} + echo "Using base/internal version code: $BASE_CODE" + echo "INTERNAL_VERSION_CODE=$BASE_CODE" >> $GITHUB_OUTPUT + echo "VERSION_CODE=$BASE_CODE" >> $GITHUB_ENV + fi - - name: Setup Fastlane - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.2' - bundler-cache: true - - - name: Build F-Droid with Fastlane + - name: Build and Deploy to Internal Track + if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true' env: VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }} - VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }} + VERSION_CODE: ${{ env.VERSION_CODE }} + run: bundle exec fastlane internal + + - name: Build F-Droid (same version code) + if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true' + env: + VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }} + VERSION_CODE: ${{ env.VERSION_CODE }} run: bundle exec fastlane fdroid_build - - name: Upload F-Droid APK artifact + - name: Generate Build Metadata & Checksums + if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true' + id: gen_metadata + run: | + set -euo pipefail + AAB=app/build/outputs/bundle/googleRelease/app-google-release.aab + APK_GOOGLE=app/build/outputs/apk/google/release/app-google-release.apk + APK_FDROID=app/build/outputs/apk/fdroid/release/app-fdroid-release.apk + SHA_AAB=$(sha256sum "$AAB" | cut -d' ' -f1) + SHA_APK_GOOGLE=$(sha256sum "$APK_GOOGLE" | cut -d' ' -f1) + SHA_APK_FDROID=$(sha256sum "$APK_FDROID" | cut -d' ' -f1) + GIT_SHA=$(git rev-parse HEAD) + BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) + cat > build-metadata.json <> $GITHUB_ENV + echo "APK_GOOGLE_SHA256=$SHA_APK_GOOGLE" >> $GITHUB_ENV + echo "APK_FDROID_SHA256=$SHA_APK_FDROID" >> $GITHUB_ENV + + - name: Upload Build Metadata Artifact + if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true' uses: actions/upload-artifact@v4 with: - name: fdroid-apk - path: app/build/outputs/apk/fdroid/release/app-fdroid-release.apk - retention-days: 1 + name: build-metadata + path: build-metadata.json + retention-days: 7 - - name: Attest F-Droid APK provenance - uses: actions/attest-build-provenance@v3 + - name: Upload F-Droid APK artifact + if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true' + uses: actions/upload-artifact@v4 with: - subject-path: app/build/outputs/apk/fdroid/release/app-fdroid-release.apk + name: fdroid-apk + path: app/build/outputs/apk/fdroid/release/app-fdroid-release.apk + retention-days: 1 - create-internal-release: + - name: Promotion Guard - Internal Must Exist + if: steps.fastlane_lane.outputs.lane != '' && github.event.inputs.dry_run != 'true' + run: | + set -e + echo "Validating internal track has an artifact before promotion..." + if ! bundle exec fastlane get_internal_track_version_code; then + echo "ERROR: No internal artifact found to promote. Ensure an internal tag was built first." >&2 + exit 1 + fi + + - name: Fetch Internal Track Version Code (for promotion) + if: steps.fastlane_lane.outputs.lane != '' && github.event.inputs.dry_run != 'true' + run: | + bundle exec fastlane get_internal_track_version_code + CODE=$(cat internal_version_code.txt) + echo "INTERNAL_VERSION_CODE=$CODE" >> $GITHUB_ENV + + - name: Promote on Google Play + if: steps.fastlane_lane.outputs.lane != '' && github.event.inputs.dry_run != 'true' + env: + VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }} + VERSION_CODE: ${{ env.INTERNAL_VERSION_CODE }} + run: bundle exec fastlane ${{ steps.fastlane_lane.outputs.lane }} + + - name: Build Summary (Internal Build) + if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true' + run: | + { + echo "### Internal Build Summary" + echo "Version Name: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}" + echo "Version Code: ${{ env.VERSION_CODE }}" + echo "Base Tag: ${{ needs.prepare-build-info.outputs.BASE_TAG }}" + echo "Full Tag: ${{ needs.prepare-build-info.outputs.FULL_TAG }}" + echo "Hotfix/Patch: ${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}" + echo "Google AAB SHA256: $AAB_SHA256" + echo "Google APK SHA256: $APK_GOOGLE_SHA256" + echo "F-Droid APK SHA256: $APK_FDROID_SHA256" + } >> $GITHUB_STEP_SUMMARY + + - name: Dry Run Summary + if: github.event.inputs.dry_run == 'true' + run: | + echo "### Release Dry Run" >> $GITHUB_STEP_SUMMARY + echo "Tag: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY + echo "Base Tag: ${{ needs.prepare-build-info.outputs.BASE_TAG }}" >> $GITHUB_STEP_SUMMARY + echo "Hotfix/Patch: ${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}" >> $GITHUB_STEP_SUMMARY + echo "Computed Version Name: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}" >> $GITHUB_STEP_SUMMARY + echo "Planned Version Code Strategy: $([[ '${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}' == 'true' ]] && echo 'highest+1 from Play' || echo 'commit-count+offset (first internal) or reuse')" >> $GITHUB_STEP_SUMMARY + echo "Sanity Highest Existing VersionCode: ${{ steps.dry_run_sanity.outputs.sanity_highest }}" >> $GITHUB_STEP_SUMMARY + echo "Sanity Planned VersionCode: ${{ steps.dry_run_sanity.outputs.sanity_planned }}" >> $GITHUB_STEP_SUMMARY + echo "Sanity Status: ${{ steps.dry_run_sanity.outputs.sanity_status }}" >> $GITHUB_STEP_SUMMARY + echo "Promotion Policy Check: ${{ steps.dry_run_sanity.outputs.promotion_status }}" >> $GITHUB_STEP_SUMMARY + echo "Would build internal artifacts: $([[ '${{ needs.check-internal-release.outputs.exists }}' == 'false' ]] && echo yes || echo 'no (already exists)')" >> $GITHUB_STEP_SUMMARY + echo "Would promote lane: $([[ -n '${{ steps.fastlane_lane.outputs.lane }}' ]] && echo '${{ steps.fastlane_lane.outputs.lane }}' || echo 'n/a')" >> $GITHUB_STEP_SUMMARY + echo "Would create or update draft GitHub release: $([[ '${{ needs.check-internal-release.outputs.exists }}' == 'false' ]] && echo yes || echo no)" >> $GITHUB_STEP_SUMMARY + + - name: Post Dry Run PR Comment + if: github.event.inputs.dry_run == 'true' && github.event.inputs.pr_number != '' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BODY=$(cat <<'EOT' + Release Dry Run Summary + ---------------------- + Tag: ${{ github.ref_name }} + Base Tag: ${{ needs.prepare-build-info.outputs.BASE_TAG }} + Version Name: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }} + Hotfix/Patch: ${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }} + Planned VersionCode: ${{ steps.dry_run_sanity.outputs.sanity_planned }} (highest existing: ${{ steps.dry_run_sanity.outputs.sanity_highest }}) + Sanity Status: ${{ steps.dry_run_sanity.outputs.sanity_status }} + Promotion Policy: ${{ steps.dry_run_sanity.outputs.promotion_status }} + Would Promote Lane: $([[ -n '${{ steps.fastlane_lane.outputs.lane }}' ]] && echo '${{ steps.fastlane_lane.outputs.lane }}' || echo 'n/a') + Draft Release Action: $([[ '${{ needs.check-internal-release.outputs.exists }}' == 'false' ]] && echo 'would create/update' || echo 'none') + EOT + ) + gh pr comment ${{ github.event.inputs.pr_number }} --body "$BODY" || echo "Failed to post PR comment (verify pr_number)" + + manage-github-release: + if: github.event.inputs.dry_run != 'true' runs-on: ubuntu-latest - needs: [prepare-build-info, release-google, release-fdroid] - if: contains(github.ref_name, '-internal') + needs: [prepare-build-info, check-internal-release, release-google] steps: - name: Download all artifacts + if: needs.check-internal-release.outputs.exists == 'false' uses: actions/download-artifact@v5 with: path: ./artifacts + - name: Compute Release Name (Channel Aware) + id: release_name + run: | + BASE='${{ needs.prepare-build-info.outputs.BASE_TAG }}' + REF='${{ github.ref_name }}' + if [[ "$REF" == *"-internal"* ]]; then + NAME="$BASE (internal)" + elif [[ "$REF" == *"-closed"* ]]; then + NAME="$BASE (closed testing)" + elif [[ "$REF" == *"-open"* ]]; then + NAME="$BASE (open beta)" + else + NAME="$BASE" + fi + echo "Computed release name: $NAME" + echo "name=$NAME" >> $GITHUB_OUTPUT + - name: Create GitHub Release + if: needs.check-internal-release.outputs.exists == 'false' uses: softprops/action-gh-release@v2 with: - tag_name: v${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }} - name: ${{ github.ref_name }} + tag_name: ${{ needs.prepare-build-info.outputs.BASE_TAG }} + name: ${{ steps.release_name.outputs.name }} generate_release_notes: true files: ./artifacts/*/* draft: true @@ -232,12 +472,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - promote-release: - runs-on: ubuntu-latest - needs: [prepare-build-info, release-google] - if: "!contains(github.ref_name, '-internal')" - steps: - - name: Determine Release Properties + - name: Determine Release Properties for Promotion + if: "!contains(github.ref_name, '-internal')" id: release_properties run: | TAG_NAME="${{ github.ref_name }}" @@ -252,12 +488,24 @@ jobs: echo "prerelease=false" >> $GITHUB_OUTPUT fi - - name: Update GitHub Release + - name: Promote GitHub Release + if: "!contains(github.ref_name, '-internal')" uses: softprops/action-gh-release@v2 with: - tag_name: v${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }} - name: ${{ github.ref_name }} + tag_name: ${{ needs.prepare-build-info.outputs.BASE_TAG }} + name: ${{ steps.release_name.outputs.name }} draft: ${{ steps.release_properties.outputs.draft }} prerelease: ${{ steps.release_properties.outputs.prerelease }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Append Metadata to Release Notes + if: needs.check-internal-release.outputs.exists == 'false' + run: | + if [ -f artifacts/build-metadata/build-metadata.json ]; then + echo "\n---\nBuild Metadata JSON:\n" > appended_notes.txt + cat artifacts/build-metadata/build-metadata.json >> appended_notes.txt + gh release edit ${{ needs.prepare-build-info.outputs.BASE_TAG }} --notes-file appended_notes.txt + fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md index 002ee6821..b202aae40 100644 --- a/RELEASE_PROCESS.md +++ b/RELEASE_PROCESS.md @@ -39,6 +39,31 @@ git tag v2.3.5-closed.1 git push origin v2.3.5-closed.1 ``` +## Hotfixes & Patch Releases + +If you need to release a hotfix or patch for a previous version (not the latest mainline), follow this process: + +- **Tagging:** Use a tag with a suffix, such as `vX.X.X-hotfix1` or `vX.X.X-patch1` (e.g., `v2.3.5-hotfix1`). +- **Uniqueness:** The release workflow uses the full tag (including suffix) for all artifact and release naming, so each hotfix/patch is uniquely identified. +- **Version Code:** The workflow automatically ensures the version code for a hotfix/patch is strictly greater than any previous release, even if the hotfix is created from an older commit. This prevents Play Store upload errors due to version code regressions. +- **Multiple Releases:** You can have multiple releases for the same base version (e.g., `v2.3.5`, `v2.3.5-hotfix1`, `v2.3.5-patch2`). Each will be published and promoted independently. + +### Hotfix Tagging Example +```bash +# On a release branch or after checking out the commit to hotfix +# Tag and push a hotfix release +git tag v2.3.5-hotfix1 +git push origin v2.3.5-hotfix1 +# For additional hotfixes/patches: +git tag v2.3.5-hotfix2 +git push origin v2.3.5-hotfix2 +``` + +### Policy +- Always use a unique tag for each hotfix/patch. +- The version code will always increase, regardless of commit history. +- The full tag is used for all release and artifact naming. + ## Manual Checklist - [ ] Verify build in Google Play Console - [ ] Review and publish GitHub draft release (for production) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 17706b512..d7b5a0a5f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -23,17 +23,25 @@ platform :android do desc "Deploy a new version to the internal track on Google Play" lane :internal do - aab_path = build_google_release - upload_to_play_store( - track: 'internal', - aab: aab_path, - release_status: 'completed', - skip_upload_apk: true, - skip_upload_metadata: true, - skip_upload_changelogs: true, - skip_upload_images: true, - skip_upload_screenshots: true, - ) + begin + aab_path = build_google_release + upload_to_play_store( + track: 'internal', + aab: aab_path, + release_status: 'completed', + skip_upload_apk: true, + skip_upload_metadata: true, + skip_upload_changelogs: true, + skip_upload_images: true, + skip_upload_screenshots: true, + ) + rescue => exception + if exception.message.include?("Google Api Error: forbidden: A release with version code") && exception.message.include?("already exists for this track") + UI.message("This version code is already on the internal track. No action needed.") + else + raise exception + end + end end desc "Promote from internal track to the closed track on Google Play" @@ -41,6 +49,7 @@ platform :android do upload_to_play_store( track: 'internal', track_promote_to: 'NewAlpha', + version_codes: [ENV["VERSION_CODE"].to_i], release_status: 'completed', skip_upload_apk: true, skip_upload_metadata: true, @@ -50,11 +59,12 @@ platform :android do ) end - desc "Promote from closed track to the open track on Google Play" + desc "Promote from internal track to the open track on Google Play" lane :open do upload_to_play_store( - track: 'NewAlpha', + track: 'internal', track_promote_to: 'beta', + version_codes: [ENV["VERSION_CODE"].to_i], release_status: 'draft', skip_upload_apk: true, skip_upload_metadata: true, @@ -64,11 +74,12 @@ platform :android do ) end - desc "Promote from open track to the production track on Google Play" + desc "Promote from internal track to the production track on Google Play" lane :production do upload_to_play_store( - track: 'open', + track: 'internal', track_promote_to: 'production', + version_codes: [ENV["VERSION_CODE"].to_i], release_status: 'draft', skip_upload_apk: true, skip_upload_metadata: true, @@ -89,6 +100,35 @@ platform :android do ) end + desc "Get the highest version code from all Google Play tracks" + lane :get_highest_version_code do + require 'set' + all_codes = Set.new + tracks = ['internal', 'closed', 'open', 'production'] + tracks.each do |track| + begin + codes = google_play_track_version_codes(track: track, package_name: 'com.geeksville.mesh') + all_codes.merge(codes.map(&:to_i)) + rescue => e + UI.message("Could not fetch version codes for track #{track}: #{e.message}") + end + end + highest = all_codes.max || 0 + UI.message("Highest version code on Google Play: #{highest}") + File.write('highest_version_code.txt', highest.to_s) + end + + desc "Get the version code currently on the internal track (max if multiple)" + lane :get_internal_track_version_code do + codes = google_play_track_version_codes(track: 'internal', package_name: 'com.geeksville.mesh') + if codes.nil? || codes.empty? + UI.user_error!("No version codes found on internal track. Ensure an internal build has been published before promoting.") + end + max_code = codes.map(&:to_i).max + UI.message("Internal track version code: #{max_code}") + File.write('internal_version_code.txt', max_code.to_s) + end + private_lane :build_google_release do gradle( task: "clean bundleGoogleRelease assembleGoogleRelease", From a7183cc8ca74faad6763c52ab0971596ea42ca2e Mon Sep 17 00:00:00 2001 From: b8b8 <156552149+b8b8@users.noreply.github.com> Date: Fri, 3 Oct 2025 08:01:19 -0700 Subject: [PATCH 9/9] Update strings.xml - PKI required for DMs (#3301) Signed-off-by: b8b8 <156552149+b8b8@users.noreply.github.com> --- core/strings/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/strings/src/main/res/values/strings.xml b/core/strings/src/main/res/values/strings.xml index 09a95805a..d75ff8c8d 100644 --- a/core/strings/src/main/res/values/strings.xml +++ b/core/strings/src/main/res/values/strings.xml @@ -355,9 +355,9 @@ Percent of airtime for transmission used within the last hour. IAQ Shared Key - Direct messages are using the shared key for the channel. + Only channel messages can be sent/received. Direct Messages require the Public Key Infrastructure feature in 2.5+ firmware. Public Key Encryption - Direct messages are using the new public key infrastructure for encryption. Requires firmware version 2.5 or greater. + Direct messages are using the new public key infrastructure for encryption. Public key mismatch The public key does not match the recorded key. You may remove the node and let it exchange keys again, but this may indicate a more serious security problem. Contact the user through another trusted channel, to determine if the key change was due to a factory reset or other intentional action. Exchange user info