diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt index 3fc2d334b..3cef37306 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt @@ -159,6 +159,8 @@ open class RadioConfigRepositoryImpl( channel_url = channels.getChannelUrl().toString(), config = localConfig, module_config = localModuleConfig, + is_unmessagable = node?.user?.is_unmessagable, + is_licensed = node?.user?.is_licensed, fixed_position = if (node != null && localConfig.position?.fixed_position == true) { node.position diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt index b0f62c490..8c63ebd2b 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt @@ -46,13 +46,17 @@ open class InstallProfileUseCase constructor(private val radioController: RadioC } } + // is_licensed is deliberately not installed here: enabling ham mode is a dedicated onboarding flow + // (set_ham_mode — rewrites the owner, disables encryption, applies tx power/frequency) that a plain + // set_owner would bypass, leaving the radio flagged licensed without those required side effects. private suspend fun AdminEditScope.installOwner(profile: DeviceProfile, currentUser: User?) { - if (profile.long_name != null || profile.short_name != null) { + if (profile.long_name != null || profile.short_name != null || profile.is_unmessagable != null) { currentUser?.let { setOwner( it.copy( long_name = profile.long_name ?: it.long_name, short_name = profile.short_name ?: it.short_name, + is_unmessagable = profile.is_unmessagable ?: it.is_unmessagable, ), ) } diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt index 689582d9b..09a193c22 100644 --- a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt @@ -45,6 +45,7 @@ import org.meshtastic.proto.ModuleConfig.TelemetryConfig import org.meshtastic.proto.User import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertTrue class InstallProfileUseCaseTest { @@ -107,4 +108,14 @@ class InstallProfileUseCaseTest { assertTrue(radioController.editSettingsCalled) } + + @Test + fun `invoke installs is_unmessagable but never auto-installs is_licensed`() = runTest { + val profile = DeviceProfile(is_unmessagable = true, is_licensed = true) + + useCase(1234, profile, User(long_name = "Old")) + + assertEquals(true, radioController.lastSetOwnerUser?.is_unmessagable) + assertEquals(false, radioController.lastSetOwnerUser?.is_licensed) + } } diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt index ebc96efeb..8a5e10a2f 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt @@ -55,6 +55,7 @@ class FakeRadioController : var throwOnSend: Boolean = false var lastSetDeviceAddress: String? = null + var lastSetOwnerUser: User? = null var editSettingsCalled = false var startProvideLocationCalled = false var stopProvideLocationCalled = false @@ -67,6 +68,7 @@ class FakeRadioController : localConfigs.clear() throwOnSend = false lastSetDeviceAddress = null + lastSetOwnerUser = null editSettingsCalled = false startProvideLocationCalled = false stopProvideLocationCalled = false @@ -107,7 +109,9 @@ class FakeRadioController : override suspend fun setLocalChannel(channel: Channel) {} - override suspend fun setOwner(destNum: Int, user: User, packetId: Int) {} + override suspend fun setOwner(destNum: Int, user: User, packetId: Int) { + lastSetOwnerUser = user + } override suspend fun setHamMode(destNum: Int, hamParameters: HamParameters, packetId: Int) {} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt index e0a110ae4..d2ebfdff4 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt @@ -31,15 +31,20 @@ import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.cancel import org.meshtastic.core.resources.channel_url import org.meshtastic.core.resources.fixed_position +import org.meshtastic.core.resources.licensed_amateur_radio import org.meshtastic.core.resources.long_name import org.meshtastic.core.resources.module_settings import org.meshtastic.core.resources.radio_configuration import org.meshtastic.core.resources.save import org.meshtastic.core.resources.short_name +import org.meshtastic.core.resources.unmessageable import org.meshtastic.core.ui.component.MeshtasticDialog import org.meshtastic.core.ui.component.SwitchPreference import org.meshtastic.proto.DeviceProfile +private const val UNMESSAGABLE_TAG = 9 +private const val LICENSED_TAG = 10 + private enum class ProfileField(val tag: Int, val labelRes: StringResource) { LONG_NAME(1, Res.string.long_name), SHORT_NAME(2, Res.string.short_name), @@ -47,6 +52,8 @@ private enum class ProfileField(val tag: Int, val labelRes: StringResource) { CONFIG(4, Res.string.radio_configuration), MODULE_CONFIG(5, Res.string.module_settings), FIXED_POSITION(6, Res.string.fixed_position), + UNMESSAGABLE(UNMESSAGABLE_TAG, Res.string.unmessageable), + LICENSED(LICENSED_TAG, Res.string.licensed_amateur_radio), } @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -69,6 +76,8 @@ fun EditDeviceProfileDialog( ProfileField.CONFIG -> deviceProfile.config != null ProfileField.MODULE_CONFIG -> deviceProfile.module_config != null ProfileField.FIXED_POSITION -> deviceProfile.fixed_position != null + ProfileField.UNMESSAGABLE -> deviceProfile.is_unmessagable != null + ProfileField.LICENSED -> deviceProfile.is_licensed != null } }, ) @@ -99,6 +108,9 @@ fun EditDeviceProfileDialog( } else { null }, + is_unmessagable = + if (state[ProfileField.UNMESSAGABLE] == true) deviceProfile.is_unmessagable else null, + is_licensed = if (state[ProfileField.LICENSED] == true) deviceProfile.is_licensed else null, ) onConfirm(result) }, @@ -115,6 +127,8 @@ fun EditDeviceProfileDialog( ProfileField.CONFIG -> deviceProfile.config != null ProfileField.MODULE_CONFIG -> deviceProfile.module_config != null ProfileField.FIXED_POSITION -> deviceProfile.fixed_position != null + ProfileField.UNMESSAGABLE -> deviceProfile.is_unmessagable != null + ProfileField.LICENSED -> deviceProfile.is_licensed != null } SwitchPreference( title = stringResource(field.labelRes),