feat(settings): wire is_unmessagable/is_licensed into DeviceProfile export/import (#6065)

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
James Rich
2026-07-02 06:53:48 -05:00
committed by GitHub
parent 6f8b595145
commit 4d50e847c9
5 changed files with 37 additions and 2 deletions

View File

@@ -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

View File

@@ -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,
),
)
}

View File

@@ -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)
}
}

View File

@@ -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) {}

View File

@@ -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),