refactor(ble): Centralize BLE logic into a core module (#4550)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-02-20 06:41:52 -06:00 committed by GitHub
parent 7a68802bc2
commit 6bfa5b5f70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
214 changed files with 3471 additions and 2405 deletions

View file

@ -32,13 +32,13 @@ import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.meshtastic.core.strings.getString
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.debug_active_filters
import org.meshtastic.core.strings.debug_filters
import org.meshtastic.core.strings.getString
import org.meshtastic.feature.settings.debugging.DebugViewModel.UiMeshLog
@RunWith(AndroidJUnit4::class)
@ -48,7 +48,6 @@ class DebugFiltersTest {
@Test
fun debugFilterBar_showsFilterButtonAndMenu() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val filterLabel = getString(Res.string.debug_filters)
composeTestRule.setContent {
var filterTexts by remember { mutableStateOf(listOf<String>()) }

View file

@ -31,7 +31,6 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.meshtastic.core.strings.getString
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@ -39,6 +38,7 @@ import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.debug_active_filters
import org.meshtastic.core.strings.debug_default_search
import org.meshtastic.core.strings.debug_filters
import org.meshtastic.core.strings.getString
import org.meshtastic.feature.settings.debugging.DebugViewModel.UiMeshLog
import org.meshtastic.feature.settings.debugging.LogSearchManager.SearchMatch
import org.meshtastic.feature.settings.debugging.LogSearchManager.SearchState

View file

@ -21,13 +21,13 @@ import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.meshtastic.core.strings.getString
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.cancel
import org.meshtastic.core.strings.getString
import org.meshtastic.core.strings.save
import org.meshtastic.proto.DeviceProfile
import org.meshtastic.proto.Position

View file

@ -24,12 +24,12 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.meshtastic.core.strings.getString
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.getString
import org.meshtastic.core.strings.i_agree
import org.meshtastic.core.strings.map_reporting
import org.meshtastic.core.strings.map_reporting_summary

View file

@ -68,10 +68,10 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.gpsDisabled
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.toDate
import org.meshtastic.core.common.util.toInstant
import org.meshtastic.core.database.DatabaseConstants
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.model.util.toDate
import org.meshtastic.core.model.util.toInstant
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.strings.Res

View file

@ -83,9 +83,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.pluralStringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.model.util.toDate
import org.meshtastic.core.model.util.toInstant
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.toDate
import org.meshtastic.core.common.util.toInstant
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.debug_clear
import org.meshtastic.core.strings.debug_decoded_payload

View file

@ -33,15 +33,15 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.meshtastic.core.common.util.nowInstant
import org.meshtastic.core.common.util.toDate
import org.meshtastic.core.common.util.toInstant
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.model.getTracerouteResponse
import org.meshtastic.core.model.util.decodeOrNull
import org.meshtastic.core.model.util.nowInstant
import org.meshtastic.core.model.util.toDate
import org.meshtastic.core.model.util.toInstant
import org.meshtastic.core.model.util.toReadableString
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
import org.meshtastic.core.strings.Res

View file

@ -23,9 +23,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.model.util.nowSeconds
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.are_you_sure

View file

@ -45,6 +45,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.StringResource
import org.json.JSONObject
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.data.repository.LocationRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.PacketRepository
@ -53,7 +54,6 @@ import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.database.model.getStringResFrom
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs

View file

@ -16,8 +16,6 @@
*/
package org.meshtastic.feature.settings.radio.component
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.compose.foundation.clickable
@ -45,7 +43,6 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
@ -53,7 +50,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
@ -64,9 +60,9 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import no.nordicsemi.android.common.core.registerReceiver
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.util.systemTimeZone
import org.meshtastic.core.model.util.toPosixString
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.accept
@ -123,6 +119,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.util.IntervalConfiguration
import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.Config
import java.time.ZoneId
private val Config.DeviceConfig.Role.description: StringResource
get() =
@ -261,20 +258,11 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
}
item {
TitledCard(title = stringResource(Res.string.time_zone)) {
val context = LocalContext.current
val appTzPosixString by
produceState(initialValue = systemTimeZone.toPosixString()) {
val receiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_TIMEZONE_CHANGED) {
value = systemTimeZone.toPosixString()
}
}
}
context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
awaitDispose { context.unregisterReceiver(receiver) }
}
var appTzPosixString by remember { mutableStateOf(ZoneId.systemDefault().toPosixString()) }
registerReceiver(IntentFilter(Intent.ACTION_TIMEZONE_CHANGED)) {
appTzPosixString = ZoneId.systemDefault().toPosixString()
}
EditTextPreference(
title = "",

View file

@ -16,7 +16,6 @@
*/
package org.meshtastic.feature.settings.radio.component
import android.Manifest
import android.annotation.SuppressLint
import android.location.Location
import android.os.Build
@ -37,9 +36,8 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.core.location.LocationCompat
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.coroutines.launch
import no.nordicsemi.android.common.permissions.ble.RequireLocation
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.Position
import org.meshtastic.core.strings.Res
@ -79,8 +77,8 @@ import org.meshtastic.feature.settings.util.gpioPins
import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.Config
@OptIn(ExperimentalPermissionsApi::class)
@Composable
@Suppress("LongMethod", "CyclomaticComplexMethod")
fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val coroutineScope = rememberCoroutineScope()
@ -114,14 +112,6 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
val formState = rememberConfigState(initialValue = sanitizedPositionConfig)
var locationInput by rememberSaveable { mutableStateOf(currentPosition) }
val locationPermissionState =
rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) { granted ->
if (granted) {
@SuppressLint("MissingPermission")
coroutineScope.launch { phoneLocation = viewModel.getCurrentLocation() }
}
}
LaunchedEffect(phoneLocation) {
phoneLocation?.let { phoneLoc ->
locationInput =
@ -262,11 +252,16 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
onValueChanged = { alt: Int -> locationInput = locationInput.copy(altitude = alt) },
)
HorizontalDivider()
TextButton(
enabled = state.connected,
onClick = { coroutineScope.launch { locationPermissionState.launchPermissionRequest() } },
) {
Text(text = stringResource(Res.string.position_config_set_fixed_from_phone))
RequireLocation { isLocationRequiredAndDisabled: Boolean ->
TextButton(
enabled = state.connected && !isLocationRequiredAndDisabled,
onClick = {
@SuppressLint("MissingPermission")
coroutineScope.launch { phoneLocation = viewModel.getCurrentLocation() }
},
) {
Text(text = stringResource(Res.string.position_config_set_fixed_from_phone))
}
}
} else {
HorizontalDivider()

View file

@ -25,7 +25,6 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Warning
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -41,8 +40,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.model.util.encodeToString
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.admin_key
import org.meshtastic.core.strings.admin_keys
@ -76,8 +75,8 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.Config
import java.security.SecureRandom
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@Suppress("LongMethod")
fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val node by viewModel.destNode.collectAsStateWithLifecycle()

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 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
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.settings.util
import androidx.appcompat.app.AppCompatDelegate
@ -37,6 +36,10 @@ object LanguageUtils {
const val SYSTEM_DEFAULT = "zz"
/**
* Sets the application locale using AppCompatDelegate. Note: This is the modern standard for per-app language
* support, providing a backport for API levels below 33.
*/
fun setAppLocale(languageTag: String) {
AppCompatDelegate.setApplicationLocales(
if (languageTag == SYSTEM_DEFAULT) {

View file

@ -22,11 +22,11 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import com.meshtastic.core.strings.getString
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.getString
import org.meshtastic.core.strings.use_homoglyph_characters_encoding
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config