From d02909df5e76ab77bd8eb5874b959ec16be890f4 Mon Sep 17 00:00:00 2001 From: Tristan Waddington Date: Mon, 28 Jul 2025 18:10:02 -0700 Subject: [PATCH] Replaced a few hardcoded string values with string resources (#2544) --- .../mesh/service/MeshServiceNotifications.kt | 428 ++++++++---------- .../components/PositionPrecisionPreference.kt | 46 +- .../geeksville/mesh/ui/metrics/PositionLog.kt | 181 +++----- .../components/EditChannelDialog.kt | 61 +-- app/src/main/res/values/strings.xml | 3 + 5 files changed, 308 insertions(+), 411 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt index cb68687e1..3f3c740e6 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt @@ -45,9 +45,7 @@ import com.geeksville.mesh.service.ReplyReceiver.Companion.KEY_TEXT_REPLY import com.geeksville.mesh.util.formatUptime @Suppress("TooManyFunctions") -class MeshServiceNotifications( - private val context: Context -) { +class MeshServiceNotifications(private val context: Context) { val notificationLightColor = Color.BLUE @@ -56,7 +54,8 @@ class MeshServiceNotifications( const val MAX_BATTERY_LEVEL = 100 } - private val notificationManager: NotificationManager get() = context.notificationManager + private val notificationManager: NotificationManager + get() = context.notificationManager // We have two notification channels: one for general service status and another one for messages val notifyId = 101 @@ -84,14 +83,11 @@ class MeshServiceNotifications( val channelId = "my_service" if (notificationManager.getNotificationChannel(channelId) == null) { val channelName = context.getString(R.string.meshtastic_service_notifications) - val channel = NotificationChannel( - channelId, - channelName, - NotificationManager.IMPORTANCE_MIN - ).apply { - lightColor = notificationLightColor - lockscreenVisibility = Notification.VISIBILITY_PRIVATE - } + val channel = + NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_MIN).apply { + lightColor = notificationLightColor + lockscreenVisibility = Notification.VISIBILITY_PRIVATE + } notificationManager.createNotificationChannel(channel) } return channelId @@ -102,22 +98,19 @@ class MeshServiceNotifications( val channelId = "my_messages" if (notificationManager.getNotificationChannel(channelId) == null) { val channelName = context.getString(R.string.meshtastic_messages_notifications) - val channel = NotificationChannel( - channelId, - channelName, - NotificationManager.IMPORTANCE_HIGH - ).apply { - lightColor = notificationLightColor - lockscreenVisibility = Notification.VISIBILITY_PUBLIC - setShowBadge(true) - setSound( - RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), - AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_NOTIFICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .build() - ) - } + val channel = + NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH).apply { + lightColor = notificationLightColor + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + setShowBadge(true) + setSound( + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(), + ) + } notificationManager.createNotificationChannel(channel) } return channelId @@ -128,22 +121,19 @@ class MeshServiceNotifications( val channelId = "my_broadcasts" if (notificationManager.getNotificationChannel(channelId) == null) { val channelName = context.getString(R.string.meshtastic_broadcast_notifications) - val channel = NotificationChannel( - channelId, - channelName, - NotificationManager.IMPORTANCE_DEFAULT - ).apply { - lightColor = notificationLightColor - lockscreenVisibility = Notification.VISIBILITY_PUBLIC - setShowBadge(true) - setSound( - RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), - AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_NOTIFICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .build() - ) - } + val channel = + NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT).apply { + lightColor = notificationLightColor + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + setShowBadge(true) + setSound( + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(), + ) + } notificationManager.createNotificationChannel(channel) } return channelId @@ -154,31 +144,31 @@ class MeshServiceNotifications( val channelId = "my_alerts" if (notificationManager.getNotificationChannel(channelId) == null) { val channelName = context.getString(R.string.meshtastic_alerts_notifications) - val channel = NotificationChannel( - channelId, - channelName, - NotificationManager.IMPORTANCE_HIGH - ).apply { - enableLights(true) - enableVibration(true) - setBypassDnd(true) - lightColor = notificationLightColor - lockscreenVisibility = Notification.VISIBILITY_PUBLIC - setShowBadge(true) - val alertSoundUri = - ( + val channel = + NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH).apply { + enableLights(true) + enableVibration(true) + setBypassDnd(true) + lightColor = notificationLightColor + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + setShowBadge(true) + val alertSoundUri = + ( ContentResolver.SCHEME_ANDROID_RESOURCE + - "://" + context.applicationContext.packageName + - "/" + R.raw.alert - ).toUri() - setSound( - alertSoundUri, - AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_NOTIFICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .build() - ) - } + "://" + + context.applicationContext.packageName + + "/" + + R.raw.alert + ) + .toUri() + setSound( + alertSoundUri, + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(), + ) + } notificationManager.createNotificationChannel(channel) } return channelId @@ -189,22 +179,19 @@ class MeshServiceNotifications( val channelId = "new_nodes" if (notificationManager.getNotificationChannel(channelId) == null) { val channelName = context.getString(R.string.meshtastic_new_nodes_notifications) - val channel = NotificationChannel( - channelId, - channelName, - NotificationManager.IMPORTANCE_HIGH - ).apply { - lightColor = notificationLightColor - lockscreenVisibility = Notification.VISIBILITY_PUBLIC - setShowBadge(true) - setSound( - RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), - AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_NOTIFICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .build() - ) - } + val channel = + NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH).apply { + lightColor = notificationLightColor + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + setShowBadge(true) + setSound( + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(), + ) + } notificationManager.createNotificationChannel(channel) } return channelId @@ -215,22 +202,19 @@ class MeshServiceNotifications( val channelId = "low_battery" if (notificationManager.getNotificationChannel(channelId) == null) { val channelName = context.getString(R.string.meshtastic_low_battery_notifications) - val channel = NotificationChannel( - channelId, - channelName, - NotificationManager.IMPORTANCE_HIGH - ).apply { - lightColor = notificationLightColor - lockscreenVisibility = Notification.VISIBILITY_PUBLIC - setShowBadge(true) - setSound( - RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), - AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_NOTIFICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .build() - ) - } + val channel = + NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH).apply { + lightColor = notificationLightColor + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + setShowBadge(true) + setSound( + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(), + ) + } notificationManager.createNotificationChannel(channel) } return channelId @@ -242,25 +226,21 @@ class MeshServiceNotifications( private fun createLowBatteryRemoteNotificationChannel(): String { val channelId = "low_battery_remote" if (notificationManager.getNotificationChannel(channelId) == null) { - val channelName = - context.getString(R.string.meshtastic_low_battery_temporary_remote_notifications) - val channel = NotificationChannel( - channelId, - channelName, - NotificationManager.IMPORTANCE_HIGH - ).apply { - lightColor = notificationLightColor - lockscreenVisibility = Notification.VISIBILITY_PUBLIC - enableVibration(true) - setShowBadge(true) - setSound( - RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), - AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_NOTIFICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .build() - ) - } + val channelName = context.getString(R.string.meshtastic_low_battery_temporary_remote_notifications) + val channel = + NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH).apply { + lightColor = notificationLightColor + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + enableVibration(true) + setShowBadge(true) + setSound( + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(), + ) + } notificationManager.createNotificationChannel(channel) } return channelId @@ -271,15 +251,12 @@ class MeshServiceNotifications( val channelId = "client_notifications" if (notificationManager.getNotificationChannel(channelId) == null) { val channelName = context.getString(R.string.client_notification) - val channel = NotificationChannel( - channelId, - channelName, - NotificationManager.IMPORTANCE_HIGH - ).apply { - lightColor = notificationLightColor - lockscreenVisibility = Notification.VISIBILITY_PUBLIC - setShowBadge(true) - } + val channel = + NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH).apply { + lightColor = notificationLightColor + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + setShowBadge(true) + } notificationManager.createNotificationChannel(channel) } return channelId @@ -355,19 +332,23 @@ class MeshServiceNotifications( } } - private fun LocalStats?.formatToString(): String = this?.allFields?.mapNotNull { (k, v) -> - when (k.name) { - "num_online_nodes", "num_total_nodes" -> return@mapNotNull null - "uptime_seconds" -> "Uptime: ${formatUptime(v as Int)}" - "channel_utilization" -> "ChUtil: %.2f%%".format(v) - "air_util_tx" -> "AirUtilTX: %.2f%%".format(v) - else -> - "${ - k.name.replace('_', ' ').split(" ") - .joinToString(" ") { it.replaceFirstChar { char -> char.uppercase() } } - }: $v" + private fun LocalStats?.formatToString(): String = this?.allFields + ?.mapNotNull { (k, v) -> + when (k.name) { + "num_online_nodes", + "num_total_nodes", + -> return@mapNotNull null + "uptime_seconds" -> "Uptime: ${formatUptime(v as Int)}" + "channel_utilization" -> "ChUtil: %.2f%%".format(v) + "air_util_tx" -> "AirUtilTX: %.2f%%".format(v) + else -> + "${ + k.name.replace('_', ' ').split(" ") + .joinToString(" ") { it.replaceFirstChar { char -> char.uppercase() } } + }: $v" + } } - }?.joinToString("\n") ?: "No Local Stats" + ?.joinToString("\n") ?: "No Local Stats" fun updateServiceStateNotification( summaryString: String? = null, @@ -379,8 +360,8 @@ class MeshServiceNotifications( createServiceStateNotification( name = summaryString.orEmpty(), message = localStats.formatToString(), - nextUpdateAt = currentStatsUpdatedAtMillis?.plus(FIFTEEN_MINUTES_IN_MILLIS) - ) + nextUpdateAt = currentStatsUpdatedAtMillis?.plus(FIFTEEN_MINUTES_IN_MILLIS), + ), ) } @@ -391,27 +372,27 @@ class MeshServiceNotifications( fun updateMessageNotification(contactKey: String, name: String, message: String, isBroadcast: Boolean) = notificationManager.notify( contactKey.hashCode(), // show unique notifications, - createMessageNotification(contactKey, name, message, isBroadcast) + createMessageNotification(contactKey, name, message, isBroadcast), ) fun showAlertNotification(contactKey: String, name: String, alert: String) { notificationManager.notify( name.hashCode(), // show unique notifications, - createAlertNotification(contactKey, name, alert) + createAlertNotification(contactKey, name, alert), ) } fun showNewNodeSeenNotification(node: NodeEntity) { notificationManager.notify( node.num, // show unique notifications - createNewNodeSeenNotification(node.user.shortName, node.user.longName) + createNewNodeSeenNotification(node.user.shortName, node.user.longName), ) } fun showOrUpdateLowBatteryNotification(node: NodeEntity, isRemote: Boolean) { notificationManager.notify( node.num, // show unique notifications - createLowBatteryNotification(node, isRemote) + createLowBatteryNotification(node, isRemote), ) } @@ -422,9 +403,10 @@ class MeshServiceNotifications( fun showClientNotification(notification: MeshProtos.ClientNotification) { notificationManager.notify( notification.toString().hashCode(), // show unique notifications - createClientNotification(context.getString(R.string.client_notification), notification.message) + createClientNotification(context.getString(R.string.client_notification), notification.message), ) } + fun clearClientNotification(notification: MeshProtos.ClientNotification) { notificationManager.cancel(notification.toString().hashCode()) } @@ -433,48 +415,40 @@ class MeshServiceNotifications( PendingIntent.getActivity( context, 0, - Intent(context, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_SINGLE_TOP - }, - PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + Intent(context, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP }, + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, ) } - private fun createMessageReplyIntent(contactKey: String): Intent { - return Intent(context, ReplyReceiver::class.java).apply { + private fun createMessageReplyIntent(contactKey: String): Intent = + Intent(context, ReplyReceiver::class.java).apply { action = ReplyReceiver.REPLY_ACTION putExtra(ReplyReceiver.CONTACT_KEY, contactKey) } - } private fun createOpenMessageIntent(contactKey: String): PendingIntent { val intentFlags = Intent.FLAG_ACTIVITY_SINGLE_TOP val deepLink = "$DEEP_LINK_BASE_URI/messages/$contactKey" - val deepLinkIntent = Intent( - Intent.ACTION_VIEW, - deepLink.toUri(), - context, - MainActivity::class.java - ).apply { - flags = intentFlags - } + val deepLinkIntent = + Intent(Intent.ACTION_VIEW, deepLink.toUri(), context, MainActivity::class.java).apply { + flags = intentFlags + } - val deepLinkPendingIntent: PendingIntent = TaskStackBuilder.create(context).run { - addNextIntentWithParentStack(deepLinkIntent) - getPendingIntent(0, PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) - } + val deepLinkPendingIntent: PendingIntent = + TaskStackBuilder.create(context).run { + addNextIntentWithParentStack(deepLinkIntent) + getPendingIntent(0, PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) + } return deepLinkPendingIntent } - private fun commonBuilder( - channel: String, - contentIntent: PendingIntent? = null - ): NotificationCompat.Builder { - val builder = NotificationCompat.Builder(context, channel) - .setDefaults(NotificationCompat.DEFAULT_ALL) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContentIntent(contentIntent ?: openAppIntent) + private fun commonBuilder(channel: String, contentIntent: PendingIntent? = null): NotificationCompat.Builder { + val builder = + NotificationCompat.Builder(context, channel) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentIntent(contentIntent ?: openAppIntent) builder.setSmallIcon( // vector form icons don't work reliably on older androids @@ -482,16 +456,17 @@ class MeshServiceNotifications( R.drawable.app_icon_novect } else { R.drawable.app_icon - } + }, ) return builder } lateinit var serviceNotificationBuilder: NotificationCompat.Builder + fun createServiceStateNotification( name: String, message: String? = null, - nextUpdateAt: Long? = null + nextUpdateAt: Long? = null, ): Notification { if (!::serviceNotificationBuilder.isInitialized) { serviceNotificationBuilder = commonBuilder(channelId) @@ -503,10 +478,7 @@ class MeshServiceNotifications( setContentTitle(name) message?.let { setContentText(it) - setStyle( - NotificationCompat.BigTextStyle() - .bigText(message), - ) + setStyle(NotificationCompat.BigTextStyle().bigText(message)) } nextUpdateAt?.let { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { @@ -514,9 +486,7 @@ class MeshServiceNotifications( setUsesChronometer(true) setChronometerCountDown(true) } - } ?: { - setWhen(System.currentTimeMillis()) - } + } ?: { setWhen(System.currentTimeMillis()) } setShowWhen(true) } return serviceNotificationBuilder.build() @@ -535,10 +505,11 @@ class MeshServiceNotifications( val person = Person.Builder().setName(name).build() // Key for the string that's delivered in the action's intent. val replyLabel: String = context.getString(R.string.reply) - val remoteInput: RemoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).run { - setLabel(replyLabel) - build() - } + val remoteInput: RemoteInput = + RemoteInput.Builder(KEY_TEXT_REPLY).run { + setLabel(replyLabel) + build() + } // Build a PendingIntent for the reply action to trigger. val replyPendingIntent: PendingIntent = @@ -546,23 +517,19 @@ class MeshServiceNotifications( context, contactKey.hashCode(), createMessageReplyIntent(contactKey), - PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, ) // Create the reply action and add the remote input. - val action: NotificationCompat.Action = NotificationCompat.Action.Builder( - android.R.drawable.ic_menu_send, - replyLabel, - replyPendingIntent - ).addRemoteInput(remoteInput).build() + val action: NotificationCompat.Action = + NotificationCompat.Action.Builder(android.R.drawable.ic_menu_send, replyLabel, replyPendingIntent) + .addRemoteInput(remoteInput) + .build() with(messageNotificationBuilder) { priority = NotificationCompat.PRIORITY_DEFAULT setCategory(Notification.CATEGORY_MESSAGE) setAutoCancel(true) - setStyle( - NotificationCompat.MessagingStyle(person) - .addMessage(message, System.currentTimeMillis(), person) - ) + setStyle(NotificationCompat.MessagingStyle(person).addMessage(message, System.currentTimeMillis(), person)) addAction(action) setWhen(System.currentTimeMillis()) setShowWhen(true) @@ -571,29 +538,23 @@ class MeshServiceNotifications( } lateinit var alertNotificationBuilder: NotificationCompat.Builder - private fun createAlertNotification( - contactKey: String, - name: String, - alert: String - ): Notification { + + private fun createAlertNotification(contactKey: String, name: String, alert: String): Notification { if (!::alertNotificationBuilder.isInitialized) { - alertNotificationBuilder = - commonBuilder(alertChannelId, createOpenMessageIntent(contactKey)) + alertNotificationBuilder = commonBuilder(alertChannelId, createOpenMessageIntent(contactKey)) } val person = Person.Builder().setName(name).build() with(alertNotificationBuilder) { priority = NotificationCompat.PRIORITY_HIGH setCategory(Notification.CATEGORY_ALARM) setAutoCancel(true) - setStyle( - NotificationCompat.MessagingStyle(person) - .addMessage(alert, System.currentTimeMillis(), person) - ) + setStyle(NotificationCompat.MessagingStyle(person).addMessage(alert, System.currentTimeMillis(), person)) } return alertNotificationBuilder.build() } lateinit var newNodeSeenNotificationBuilder: NotificationCompat.Builder + private fun createNewNodeSeenNotification(name: String, message: String? = null): Notification { if (!::newNodeSeenNotificationBuilder.isInitialized) { newNodeSeenNotificationBuilder = commonBuilder(newNodeChannelId) @@ -602,13 +563,10 @@ class MeshServiceNotifications( priority = NotificationCompat.PRIORITY_DEFAULT setCategory(Notification.CATEGORY_STATUS) setAutoCancel(true) - setContentTitle("New Node Seen: $name") + setContentTitle(context.getString(R.string.new_node_seen).format(name)) message?.let { setContentText(it) - setStyle( - NotificationCompat.BigTextStyle() - .bigText(message), - ) + setStyle(NotificationCompat.BigTextStyle().bigText(message)) } setWhen(System.currentTimeMillis()) setShowWhen(true) @@ -618,18 +576,20 @@ class MeshServiceNotifications( lateinit var lowBatteryRemoteNotificationBuilder: NotificationCompat.Builder lateinit var lowBatteryNotificationBuilder: NotificationCompat.Builder + private fun createLowBatteryNotification(node: NodeEntity, isRemote: Boolean): Notification { - val tempNotificationBuilder: NotificationCompat.Builder = if (isRemote) { - if (!::lowBatteryRemoteNotificationBuilder.isInitialized) { - lowBatteryRemoteNotificationBuilder = commonBuilder(lowBatteryChannelId) + val tempNotificationBuilder: NotificationCompat.Builder = + if (isRemote) { + if (!::lowBatteryRemoteNotificationBuilder.isInitialized) { + lowBatteryRemoteNotificationBuilder = commonBuilder(lowBatteryChannelId) + } + lowBatteryRemoteNotificationBuilder + } else { + if (!::lowBatteryNotificationBuilder.isInitialized) { + lowBatteryNotificationBuilder = commonBuilder(lowBatteryRemoteChannelId) + } + lowBatteryNotificationBuilder } - lowBatteryRemoteNotificationBuilder - } else { - if (!::lowBatteryNotificationBuilder.isInitialized) { - lowBatteryNotificationBuilder = commonBuilder(lowBatteryRemoteChannelId) - } - lowBatteryNotificationBuilder - } with(tempNotificationBuilder) { priority = NotificationCompat.PRIORITY_DEFAULT setCategory(Notification.CATEGORY_STATUS) @@ -638,21 +598,12 @@ class MeshServiceNotifications( setOnlyAlertOnce(true) setWhen(System.currentTimeMillis()) setProgress(MAX_BATTERY_LEVEL, node.deviceMetrics.batteryLevel, false) - setContentTitle( - context.getString(R.string.low_battery_title).format( - node.shortName - ) - ) - val message = context.getString(R.string.low_battery_message).format( - node.longName, - node.deviceMetrics.batteryLevel - ) + setContentTitle(context.getString(R.string.low_battery_title).format(node.shortName)) + val message = + context.getString(R.string.low_battery_message).format(node.longName, node.deviceMetrics.batteryLevel) message.let { setContentText(it) - setStyle( - NotificationCompat.BigTextStyle() - .bigText(it), - ) + setStyle(NotificationCompat.BigTextStyle().bigText(it)) } } if (isRemote) { @@ -677,10 +628,7 @@ class MeshServiceNotifications( setContentTitle(name) message?.let { setContentText(it) - setStyle( - NotificationCompat.BigTextStyle() - .bigText(message), - ) + setStyle(NotificationCompat.BigTextStyle().bigText(message)) } } return clientNotificationBuilder.build() diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/PositionPrecisionPreference.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/PositionPrecisionPreference.kt index 5a19ddd8d..14093f2ad 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/PositionPrecisionPreference.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/PositionPrecisionPreference.kt @@ -28,27 +28,28 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.geeksville.mesh.R import com.geeksville.mesh.util.DistanceUnit import com.geeksville.mesh.util.toDistanceString import kotlin.math.pow import kotlin.math.roundToInt -private const val PositionEnabled = 32 -private const val PositionDisabled = 0 +private const val POSITION_ENABLED = 32 +private const val POSITION_DISABLED = 0 -private const val PositionPrecisionMin = 10 -private const val PositionPrecisionMax = 19 -private const val PositionPrecisionDefault = 13 +private const val POSITION_PRECISION_MIN = 10 +private const val POSITION_PRECISION_MAX = 19 +private const val POSITION_PRECISION_DEFAULT = 13 @Suppress("MagicNumber") fun precisionBitsToMeters(bits: Int): Double = 23905787.925008 * 0.5.pow(bits.toDouble()) @Composable fun PositionPrecisionPreference( - title: String, value: Int, enabled: Boolean, onValueChanged: (Int) -> Unit, @@ -58,37 +59,35 @@ fun PositionPrecisionPreference( Column(modifier = modifier) { SwitchPreference( - title = title, - checked = value != PositionDisabled, + title = stringResource(R.string.position_enabled), + checked = value != POSITION_DISABLED, enabled = enabled, onCheckedChange = { enabled -> - val newValue = if (enabled) PositionEnabled else PositionDisabled + val newValue = if (enabled) POSITION_ENABLED else POSITION_DISABLED onValueChanged(newValue) }, - padding = PaddingValues(0.dp) + padding = PaddingValues(0.dp), ) - AnimatedVisibility(visible = value != PositionDisabled) { + AnimatedVisibility(visible = value != POSITION_DISABLED) { SwitchPreference( - title = "Precise location", - checked = value == PositionEnabled, + title = stringResource(R.string.precise_location), + checked = value == POSITION_ENABLED, enabled = enabled, onCheckedChange = { enabled -> - val newValue = if (enabled) PositionEnabled else PositionPrecisionDefault + val newValue = if (enabled) POSITION_ENABLED else POSITION_PRECISION_DEFAULT onValueChanged(newValue) }, - padding = PaddingValues(0.dp) + padding = PaddingValues(0.dp), ) } - AnimatedVisibility(visible = value in (PositionDisabled + 1).. viewModel.savePositionCSV(uri) } + val exportPositionLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == Activity.RESULT_OK) { + it.data?.data?.let { uri -> viewModel.savePositionCSV(uri) } + } } - } - var clearButtonEnabled by rememberSaveable(state.positionLogs) { - mutableStateOf(state.positionLogs.isNotEmpty()) - } + var clearButtonEnabled by rememberSaveable(state.positionLogs) { mutableStateOf(state.positionLogs.isNotEmpty()) } BoxWithConstraints { val compactWidth = maxWidth < 600.dp Column { - val textStyle = if (compactWidth) { - MaterialTheme.typography.bodySmall - } else { - LocalTextStyle.current - } + val textStyle = + if (compactWidth) { + MaterialTheme.typography.bodySmall + } else { + LocalTextStyle.current + } CompositionLocalProvider(LocalTextStyle provides textStyle) { HeaderItem(compactWidth) PositionList(compactWidth, state.positionLogs, state.displayUnits) @@ -241,11 +210,12 @@ fun PositionLogScreen( }, saveButtonEnabled = state.hasPositionLogs(), onSave = { - val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/*" - putExtra(Intent.EXTRA_TITLE, "position.csv") - } + val intent = + Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/*" + putExtra(Intent.EXTRA_TITLE, "position.csv") + } exportPositionLauncher.launch(intent) }, ) @@ -259,28 +229,24 @@ private fun ColumnScope.PositionList( positions: List, displayUnits: DisplayUnits, ) { - val dateFormat = remember { - DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) - } + val dateFormat = remember { DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) } - LazyColumn( - modifier = Modifier.weight(1f), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - items(positions) { position -> - PositionItem(compactWidth, position, dateFormat, displayUnits) - } + LazyColumn(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) { + items(positions) { position -> PositionItem(compactWidth, position, dateFormat, displayUnits) } } } @Suppress("MagicNumber") -private val testPosition = MeshProtos.Position.newBuilder().apply { - latitudeI = 297604270 - longitudeI = -953698040 - altitude = 1230 - satsInView = 7 - time = (System.currentTimeMillis() / 1000).toInt() -}.build() +private val testPosition = + MeshProtos.Position.newBuilder() + .apply { + latitudeI = 297604270 + longitudeI = -953698040 + altitude = 1230 + satsInView = 7 + time = (System.currentTimeMillis() / 1000).toInt() + } + .build() @Preview(showBackground = true) @Composable @@ -300,12 +266,7 @@ private fun PositionItemPreview() { private fun ActionButtonsPreview() { AppTheme { Column(Modifier.fillMaxSize(), Arrangement.Bottom) { - ActionButtons( - clearButtonEnabled = true, - onClear = {}, - saveButtonEnabled = true, - onSave = {}, - ) + ActionButtons(clearButtonEnabled = true, onClear = {}, saveButtonEnabled = true, onSave = {}) } } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/EditChannelDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/EditChannelDialog.kt index 39870ac36..0575a824c 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/EditChannelDialog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/EditChannelDialog.kt @@ -77,15 +77,15 @@ fun EditChannelDialog( maxSize = 11, // name max_size:12 enabled = true, isError = false, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Text, imeAction = ImeAction.Done - ), + keyboardOptions = + KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { - channelInput = channelInput.copy { - name = it.trim() - if (psk == Channel.default.settings.psk) psk = Channel.getRandomKey() - } + channelInput = + channelInput.copy { + name = it.trim() + if (psk == Channel.default.settings.psk) psk = Channel.getRandomKey() + } }, onFocusChanged = { isFocused = it.isFocused }, ) @@ -101,33 +101,26 @@ fun EditChannelDialog( channelInput = channelInput.copy { psk = it } } }, - onGenerateKey = { - channelInput = channelInput.copy { psk = Channel.getRandomKey() } - }, + onGenerateKey = { channelInput = channelInput.copy { psk = Channel.getRandomKey() } }, ) SwitchPreference( title = stringResource(R.string.uplink_enabled), checked = channelInput.uplinkEnabled, enabled = true, - onCheckedChange = { - channelInput = channelInput.copy { uplinkEnabled = it } - }, - padding = PaddingValues(0.dp) + onCheckedChange = { channelInput = channelInput.copy { uplinkEnabled = it } }, + padding = PaddingValues(0.dp), ) SwitchPreference( title = stringResource(R.string.downlink_enabled), checked = channelInput.downlinkEnabled, enabled = true, - onCheckedChange = { - channelInput = channelInput.copy { downlinkEnabled = it } - }, - padding = PaddingValues(0.dp) + onCheckedChange = { channelInput = channelInput.copy { downlinkEnabled = it } }, + padding = PaddingValues(0.dp), ) PositionPrecisionPreference( - title = stringResource(R.string.position_enabled), enabled = true, value = channelInput.moduleSettings.positionPrecision, onValueChanged = { @@ -139,24 +132,17 @@ fun EditChannelDialog( }, confirmButton = { FlowRow( - modifier = modifier - .fillMaxWidth() - .padding(start = 24.dp, end = 24.dp, bottom = 16.dp), + modifier = modifier.fillMaxWidth().padding(start = 24.dp, end = 24.dp, bottom = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - TextButton( - modifier = modifier.weight(1f), - onClick = onDismissRequest - ) { Text(stringResource(R.string.cancel)) } - Button( - modifier = modifier.weight(1f), - onClick = { - onAddClick(channelInput) - }, - enabled = true, - ) { Text(stringResource(R.string.save)) } + TextButton(modifier = modifier.weight(1f), onClick = onDismissRequest) { + Text(stringResource(R.string.cancel)) + } + Button(modifier = modifier.weight(1f), onClick = { onAddClick(channelInput) }, enabled = true) { + Text(stringResource(R.string.save)) + } } - } + }, ) } @@ -164,11 +150,12 @@ fun EditChannelDialog( @Composable private fun EditChannelDialogPreview() { EditChannelDialog( - channelSettings = channelSettings { + channelSettings = + channelSettings { psk = Channel.default.settings.psk name = Channel.default.name }, - onAddClick = { }, - onDismissRequest = { }, + onAddClick = {}, + onDismissRequest = {}, ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b27ba0269..81bdfa56c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -138,6 +138,7 @@ Pairing failed, please select again Location access is turned off, can not provide position to mesh. Share + New Node Seen: %s Disconnected Device sleeping Connected: %1$s online @@ -435,6 +436,7 @@ Downlink enabled Default Position enabled + Precise location GPIO pin Type Hide password @@ -653,6 +655,7 @@ Firmware version Timestamp Heading + Speed Sats Alt Freq