mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(analytics): consolidate consent logic, move to Settings (#2885)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
86ce659bc6
commit
ad736116a7
10 changed files with 172 additions and 214 deletions
|
|
@ -20,7 +20,8 @@ package com.geeksville.mesh.repository.radio
|
|||
import com.geeksville.mesh.android.Logging
|
||||
|
||||
/**
|
||||
* An interface that assumes we are talking to a meshtastic device over some sort of stream connection (serial or TCP probably)
|
||||
* An interface that assumes we are talking to a meshtastic device over some sort of stream connection (serial or TCP
|
||||
* probably)
|
||||
*/
|
||||
abstract class StreamInterface(protected val service: RadioInterfaceService) :
|
||||
Logging,
|
||||
|
|
@ -46,12 +47,16 @@ abstract class StreamInterface(protected val service: RadioInterfaceService) :
|
|||
onDeviceDisconnect(true)
|
||||
}
|
||||
|
||||
/** Tell MeshService our device has gone away, but wait for it to come back
|
||||
/**
|
||||
* Tell MeshService our device has gone away, but wait for it to come back
|
||||
*
|
||||
* @param waitForStopped if true we should wait for the manager to finish - must be false if called from inside the manager callbacks
|
||||
* */
|
||||
* @param waitForStopped if true we should wait for the manager to finish - must be false if called from inside the
|
||||
* manager callbacks
|
||||
*/
|
||||
protected open fun onDeviceDisconnect(waitForStopped: Boolean) {
|
||||
service.onDisconnect(isPermanent = true) // if USB device disconnects it is definitely permanently gone, not sleeping)
|
||||
service.onDisconnect(
|
||||
isPermanent = true,
|
||||
) // if USB device disconnects it is definitely permanently gone, not sleeping)
|
||||
}
|
||||
|
||||
protected open fun connect() {
|
||||
|
|
@ -85,14 +90,12 @@ abstract class StreamInterface(protected val service: RadioInterfaceService) :
|
|||
/** Print device serial debug output somewhere */
|
||||
private fun debugOut(b: Byte) {
|
||||
when (val c = b.toChar()) {
|
||||
'\r' -> {
|
||||
} // ignore
|
||||
'\r' -> {} // ignore
|
||||
'\n' -> {
|
||||
debug("DeviceLog: $debugLineBuf")
|
||||
debugLineBuf.clear()
|
||||
}
|
||||
else ->
|
||||
debugLineBuf.append(c)
|
||||
else -> debugLineBuf.append(c)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,16 +136,19 @@ abstract class StreamInterface(protected val service: RadioInterfaceService) :
|
|||
// We've read our header, do one big read for the packet itself
|
||||
packetLen = (msb shl 8) or lsb
|
||||
if (packetLen > MAX_TO_FROM_RADIO_SIZE) {
|
||||
lostSync() // If packet len is too long, the bytes must have been corrupted, start looking for START1 again
|
||||
lostSync() // If packet len is too long, the bytes must have been corrupted, start looking for
|
||||
// START1 again
|
||||
} else if (packetLen == 0) {
|
||||
deliverPacket() // zero length packets are valid and should be delivered immediately (because there won't be a next byte of payload)
|
||||
deliverPacket() // zero length packets are valid and should be delivered immediately (because there
|
||||
// won't be a next byte of payload)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// We are looking at the packet bytes now
|
||||
rxPacket[ptr - 4] = c
|
||||
|
||||
// Note: we have to check if ptr +1 is equal to packet length (for example, for a 1 byte packetlen, this code will be run with ptr of4
|
||||
// Note: we have to check if ptr +1 is equal to packet length (for example, for a 1 byte packetlen, this
|
||||
// code will be run with ptr of4
|
||||
if (ptr - 4 + 1 == packetLen) {
|
||||
deliverPacket()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,13 @@
|
|||
|
||||
package com.geeksville.mesh.ui.common.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.material3.CircularWavyProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.Switch
|
||||
|
|
@ -30,6 +34,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun SwitchPreference(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -40,46 +45,47 @@ fun SwitchPreference(
|
|||
onCheckedChange: (Boolean) -> Unit,
|
||||
padding: PaddingValues? = null,
|
||||
containerColor: Color? = null,
|
||||
loading: Boolean = false,
|
||||
) {
|
||||
ListItem(
|
||||
colors = ListItemDefaults.colors().copy(
|
||||
headlineColor = if (enabled) {
|
||||
ListItemDefaults.colors().headlineColor
|
||||
} else {
|
||||
ListItemDefaults.colors().headlineColor.copy(alpha = 0.5f)
|
||||
},
|
||||
supportingTextColor = if (enabled) {
|
||||
ListItemDefaults.colors().supportingTextColor
|
||||
} else {
|
||||
ListItemDefaults.colors().supportingTextColor.copy(alpha = 0.5f)
|
||||
},
|
||||
containerColor = containerColor ?: ListItemDefaults.colors().containerColor,
|
||||
),
|
||||
modifier = (padding?.let { Modifier.padding(it) } ?: modifier).toggleable(
|
||||
colors =
|
||||
ListItemDefaults.colors()
|
||||
.copy(
|
||||
headlineColor =
|
||||
if (enabled) {
|
||||
ListItemDefaults.colors().headlineColor
|
||||
} else {
|
||||
ListItemDefaults.colors().headlineColor.copy(alpha = 0.5f)
|
||||
},
|
||||
supportingTextColor =
|
||||
if (enabled) {
|
||||
ListItemDefaults.colors().supportingTextColor
|
||||
} else {
|
||||
ListItemDefaults.colors().supportingTextColor.copy(alpha = 0.5f)
|
||||
},
|
||||
containerColor = containerColor ?: ListItemDefaults.colors().containerColor,
|
||||
),
|
||||
modifier =
|
||||
(padding?.let { Modifier.padding(it) } ?: modifier).toggleable(
|
||||
value = checked,
|
||||
enabled = enabled,
|
||||
onValueChange = onCheckedChange,
|
||||
),
|
||||
trailingContent = {
|
||||
Switch(
|
||||
enabled = enabled,
|
||||
checked = checked,
|
||||
onCheckedChange = null,
|
||||
)
|
||||
AnimatedContent(targetState = loading) { loading ->
|
||||
if (loading) {
|
||||
CircularWavyProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
} else {
|
||||
Switch(enabled = enabled, checked = checked, onCheckedChange = null)
|
||||
}
|
||||
}
|
||||
},
|
||||
supportingContent = {
|
||||
if (summary.isNotEmpty()) {
|
||||
Text(
|
||||
text = summary,
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
)
|
||||
Text(text = summary, modifier = Modifier.padding(bottom = 16.dp))
|
||||
}
|
||||
},
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = title,
|
||||
)
|
||||
}
|
||||
headlineContent = { Text(text = title) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bluetooth
|
||||
|
|
@ -49,7 +48,6 @@ import androidx.compose.material.icons.rounded.Bluetooth
|
|||
import androidx.compose.material.icons.rounded.Usb
|
||||
import androidx.compose.material.icons.rounded.Wifi
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
|
|
@ -71,7 +69,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -83,10 +80,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.geeksville.mesh.BuildConfig
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.android.GeeksvilleApplication
|
||||
import com.geeksville.mesh.android.gpsDisabled
|
||||
import com.geeksville.mesh.android.isGooglePlayAvailable
|
||||
import com.geeksville.mesh.model.BTScanModel
|
||||
import com.geeksville.mesh.model.BluetoothViewModel
|
||||
import com.geeksville.mesh.model.DeviceListEntry
|
||||
|
|
@ -141,7 +135,6 @@ fun ConnectionsScreen(
|
|||
val connectionState by uiViewModel.connectionState.collectAsStateWithLifecycle(ConnectionState.DISCONNECTED)
|
||||
val scanning by scanModel.spinner.collectAsStateWithLifecycle(false)
|
||||
val context = LocalContext.current
|
||||
val app = (context.applicationContext as GeeksvilleApplication)
|
||||
val info by uiViewModel.myNodeInfo.collectAsStateWithLifecycle()
|
||||
val selectedDevice by scanModel.selectedNotNullFlow.collectAsStateWithLifecycle()
|
||||
val bluetoothEnabled by bluetoothViewModel.enabled.collectAsStateWithLifecycle(false)
|
||||
|
|
@ -425,42 +418,6 @@ fun ConnectionsScreen(
|
|||
|
||||
LaunchedEffect(Unit) { uiViewModel.suppressNoPairedWarning() }
|
||||
}
|
||||
|
||||
// Analytics Okay Checkbox
|
||||
|
||||
val isGooglePlayAvailable = context.isGooglePlayAvailable
|
||||
val isAnalyticsAllowed = app.isAnalyticsAllowed && isGooglePlayAvailable
|
||||
if (isGooglePlayAvailable) {
|
||||
var loading by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(isAnalyticsAllowed) { loading = false }
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = isAnalyticsAllowed,
|
||||
onValueChange = {
|
||||
debug("User changed analytics to $it")
|
||||
app.isAnalyticsAllowed = it
|
||||
loading = true
|
||||
},
|
||||
role = Role.Checkbox,
|
||||
enabled = isGooglePlayAvailable && !loading,
|
||||
)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Checkbox(
|
||||
enabled = isGooglePlayAvailable,
|
||||
checked = isAnalyticsAllowed,
|
||||
onCheckedChange = null,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.analytics_okay),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,28 +17,23 @@
|
|||
|
||||
package com.geeksville.mesh.ui.settings.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.rounded.Android
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
|
@ -67,6 +62,7 @@ fun SettingsItem(
|
|||
}
|
||||
|
||||
/** A toggleable settings switch item. */
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun SettingsItemSwitch(
|
||||
checked: Boolean,
|
||||
|
|
@ -120,17 +116,14 @@ private fun ClickableWrapper(enabled: Boolean, onClick: () -> Unit, content: @Co
|
|||
|
||||
/** The row content to display for a settings item. */
|
||||
@Composable
|
||||
private fun Content(leading: @Composable () -> Unit, text: String, trailing: @Composable RowScope.() -> Unit) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp, horizontal = 16.dp),
|
||||
) {
|
||||
leading()
|
||||
Text(text = text, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.wrapContentWidth())
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
trailing()
|
||||
}
|
||||
private fun Content(leading: @Composable () -> Unit, text: String, trailing: @Composable () -> Unit) {
|
||||
ListItem(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
headlineContent = { Text(text) },
|
||||
leadingContent = { leading() },
|
||||
trailingContent = { trailing() },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import androidx.compose.foundation.lazy.items
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.twotone.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.BugReport
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.Upload
|
||||
import androidx.compose.material.icons.twotone.Warning
|
||||
|
|
@ -79,6 +80,7 @@ import com.geeksville.mesh.ui.common.components.TitledCard
|
|||
import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
|
||||
import com.geeksville.mesh.ui.settings.components.SettingsItem
|
||||
import com.geeksville.mesh.ui.settings.components.SettingsItemSwitch
|
||||
import com.geeksville.mesh.ui.settings.radio.components.EditDeviceProfileDialog
|
||||
import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -201,6 +203,7 @@ fun RadioConfigScreen(
|
|||
deviceProfile = null
|
||||
showEditDeviceProfileDialog = true
|
||||
},
|
||||
onToggleAnalytics = { viewModel.toggleAnalytics() },
|
||||
onNavigate = onNavigate,
|
||||
)
|
||||
}
|
||||
|
|
@ -286,6 +289,7 @@ private fun RadioConfigItemList(
|
|||
onRouteClick: (Enum<*>) -> Unit = {},
|
||||
onImport: () -> Unit = {},
|
||||
onExport: () -> Unit = {},
|
||||
onToggleAnalytics: () -> Unit = {},
|
||||
onNavigate: (Route) -> Unit,
|
||||
) {
|
||||
val enabled = state.connected && !state.responseState.isWaiting() && !isManaged
|
||||
|
|
@ -364,6 +368,18 @@ private fun RadioConfigItemList(
|
|||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
if (state.analyticsAvailable) {
|
||||
TitledCard(title = stringResource(R.string.phone_settings), modifier = Modifier.padding(top = 16.dp)) {
|
||||
SettingsItemSwitch(
|
||||
text = stringResource(R.string.analytics_okay),
|
||||
checked = state.analyticsEnabled,
|
||||
leadingIcon = Icons.Default.BugReport,
|
||||
onClick = onToggleAnalytics,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,10 @@ import com.geeksville.mesh.ModuleConfigProtos
|
|||
import com.geeksville.mesh.Portnums
|
||||
import com.geeksville.mesh.Position
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.GeeksvilleApplication
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.android.isAnalyticsAvailable
|
||||
import com.geeksville.mesh.android.prefs.AnalyticsPrefs
|
||||
import com.geeksville.mesh.android.prefs.MapConsentPrefs
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
|
|
@ -92,6 +95,8 @@ data class RadioConfigState(
|
|||
val ringtone: String = "",
|
||||
val cannedMessageMessages: String = "",
|
||||
val responseState: ResponseState<Boolean> = ResponseState.Empty,
|
||||
val analyticsAvailable: Boolean = true,
|
||||
val analyticsEnabled: Boolean = false,
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
|
|
@ -103,6 +108,7 @@ constructor(
|
|||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val locationRepository: LocationRepository,
|
||||
private val mapConsentPrefs: MapConsentPrefs,
|
||||
private val analyticsPrefs: AnalyticsPrefs,
|
||||
) : ViewModel(),
|
||||
Logging {
|
||||
private val meshService: IMeshService?
|
||||
|
|
@ -159,6 +165,8 @@ constructor(
|
|||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
_radioConfigState.update { it.copy(analyticsAvailable = (app as GeeksvilleApplication).isAnalyticsAvailable) }
|
||||
|
||||
debug("RadioConfigViewModel created")
|
||||
}
|
||||
|
||||
|
|
@ -695,4 +703,9 @@ constructor(
|
|||
requestIds.update { it.apply { remove(data.requestId) } }
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleAnalytics() {
|
||||
analyticsPrefs.analyticsAllowed = !analyticsPrefs.analyticsAllowed
|
||||
_radioConfigState.update { it.copy(analyticsEnabled = analyticsPrefs.analyticsAllowed) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue