mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Replaced a few hardcoded string values with string resources (#2544)
This commit is contained in:
parent
fe97cbc0ac
commit
d02909df5e
5 changed files with 308 additions and 411 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)..<PositionEnabled) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
AnimatedVisibility(visible = value in (POSITION_DISABLED + 1)..<POSITION_ENABLED) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Slider(
|
||||
value = value.toFloat(),
|
||||
onValueChange = { onValueChanged(it.roundToInt()) },
|
||||
enabled = enabled,
|
||||
valueRange = PositionPrecisionMin.toFloat()..PositionPrecisionMax.toFloat(),
|
||||
steps = PositionPrecisionMax - PositionPrecisionMin - 1,
|
||||
valueRange = POSITION_PRECISION_MIN.toFloat()..POSITION_PRECISION_MAX.toFloat(),
|
||||
steps = POSITION_PRECISION_MAX - POSITION_PRECISION_MIN - 1,
|
||||
)
|
||||
|
||||
val precisionMeters = precisionBitsToMeters(value).toInt()
|
||||
|
|
@ -108,10 +107,9 @@ fun PositionPrecisionPreference(
|
|||
@Composable
|
||||
private fun PositionPrecisionPreferencePreview() {
|
||||
PositionPrecisionPreference(
|
||||
title = "Position enabled",
|
||||
value = PositionPrecisionDefault,
|
||||
value = POSITION_PRECISION_DEFAULT,
|
||||
enabled = true,
|
||||
onValueChanged = {},
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,34 +82,29 @@ private fun RowScope.PositionText(text: String, weight: Float) {
|
|||
)
|
||||
}
|
||||
|
||||
private const val Weight10 = .10f
|
||||
private const val Weight15 = .15f
|
||||
private const val Weight20 = .20f
|
||||
private const val Weight40 = .40f
|
||||
private const val WEIGHT_10 = .10f
|
||||
private const val WEIGHT_15 = .15f
|
||||
private const val WEIGHT_20 = .20f
|
||||
private const val WEIGHT_40 = .40f
|
||||
|
||||
@Composable
|
||||
private fun HeaderItem(compactWidth: Boolean) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
PositionText(stringResource(R.string.latitude), Weight20)
|
||||
PositionText(stringResource(R.string.longitude), Weight20)
|
||||
PositionText(stringResource(R.string.sats), Weight10)
|
||||
PositionText(stringResource(R.string.alt), Weight15)
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(8.dp), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
PositionText(stringResource(R.string.latitude), WEIGHT_20)
|
||||
PositionText(stringResource(R.string.longitude), WEIGHT_20)
|
||||
PositionText(stringResource(R.string.sats), WEIGHT_10)
|
||||
PositionText(stringResource(R.string.alt), WEIGHT_15)
|
||||
if (!compactWidth) {
|
||||
PositionText("Speed", Weight15)
|
||||
PositionText(stringResource(R.string.heading), Weight15)
|
||||
PositionText(stringResource(R.string.speed), WEIGHT_15)
|
||||
PositionText(stringResource(R.string.heading), WEIGHT_15)
|
||||
}
|
||||
PositionText(stringResource(R.string.timestamp), Weight40)
|
||||
PositionText(stringResource(R.string.timestamp), WEIGHT_40)
|
||||
}
|
||||
}
|
||||
|
||||
private const val DegD = 1e-7
|
||||
private const val HeadingDeg = 1e-5
|
||||
private const val SecondsToMillis = 1000L
|
||||
private const val DEG_D = 1e-7
|
||||
private const val HEADING_DEG = 1e-5
|
||||
private const val SECONDS_TO_MILLIS = 1000L
|
||||
|
||||
@Composable
|
||||
private fun PositionItem(
|
||||
|
|
@ -119,36 +114,32 @@ private fun PositionItem(
|
|||
system: DisplayUnits,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
PositionText("%.5f".format(position.latitudeI * DegD), Weight20)
|
||||
PositionText("%.5f".format(position.longitudeI * DegD), Weight20)
|
||||
PositionText(position.satsInView.toString(), Weight10)
|
||||
PositionText(position.altitude.metersIn(system).toString(system), Weight15)
|
||||
PositionText("%.5f".format(position.latitudeI * DEG_D), WEIGHT_20)
|
||||
PositionText("%.5f".format(position.longitudeI * DEG_D), WEIGHT_20)
|
||||
PositionText(position.satsInView.toString(), WEIGHT_10)
|
||||
PositionText(position.altitude.metersIn(system).toString(system), WEIGHT_15)
|
||||
if (!compactWidth) {
|
||||
PositionText("${position.groundSpeed} Km/h", Weight15)
|
||||
PositionText("%.0f°".format(position.groundTrack * HeadingDeg), Weight15)
|
||||
PositionText("${position.groundSpeed} Km/h", WEIGHT_15)
|
||||
PositionText("%.0f°".format(position.groundTrack * HEADING_DEG), WEIGHT_15)
|
||||
}
|
||||
PositionText(formatPositionTime(position, dateFormat), Weight40)
|
||||
PositionText(formatPositionTime(position, dateFormat), WEIGHT_40)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun formatPositionTime(
|
||||
position: MeshProtos.Position,
|
||||
dateFormat: DateFormat
|
||||
): String {
|
||||
private fun formatPositionTime(position: MeshProtos.Position, dateFormat: DateFormat): String {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val sixMonthsAgo = currentTime - 180.days.inWholeMilliseconds
|
||||
val isOlderThanSixMonths = position.time * SecondsToMillis < sixMonthsAgo
|
||||
val timeText = if (isOlderThanSixMonths) {
|
||||
stringResource(id = R.string.unknown_age)
|
||||
} else {
|
||||
dateFormat.format(position.time * SecondsToMillis)
|
||||
}
|
||||
val isOlderThanSixMonths = position.time * SECONDS_TO_MILLIS < sixMonthsAgo
|
||||
val timeText =
|
||||
if (isOlderThanSixMonths) {
|
||||
stringResource(id = R.string.unknown_age)
|
||||
} else {
|
||||
dateFormat.format(position.time * SECONDS_TO_MILLIS)
|
||||
}
|
||||
return timeText
|
||||
}
|
||||
|
||||
|
|
@ -161,9 +152,7 @@ private fun ActionButtons(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
FlowRow(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
modifier = modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
|
|
@ -171,63 +160,43 @@ private fun ActionButtons(
|
|||
modifier = Modifier.weight(1f),
|
||||
onClick = onClear,
|
||||
enabled = clearButtonEnabled,
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.error),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = stringResource(id = R.string.clear),
|
||||
)
|
||||
Icon(imageVector = Icons.Default.Delete, contentDescription = stringResource(id = R.string.clear))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.clear),
|
||||
)
|
||||
Text(text = stringResource(id = R.string.clear))
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onSave,
|
||||
enabled = saveButtonEnabled,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Save,
|
||||
contentDescription = stringResource(id = R.string.save),
|
||||
)
|
||||
OutlinedButton(modifier = Modifier.weight(1f), onClick = onSave, enabled = saveButtonEnabled) {
|
||||
Icon(imageVector = Icons.Default.Save, contentDescription = stringResource(id = R.string.save))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.save),
|
||||
)
|
||||
Text(text = stringResource(id = R.string.save))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PositionLogScreen(
|
||||
viewModel: MetricsViewModel = hiltViewModel(),
|
||||
) {
|
||||
fun PositionLogScreen(viewModel: MetricsViewModel = hiltViewModel()) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
val exportPositionLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.let { uri -> 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<MeshProtos.Position>,
|
||||
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 = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@
|
|||
<string name="pairing_failed_try_again">Pairing failed, please select again</string>
|
||||
<string name="location_disabled">Location access is turned off, can not provide position to mesh.</string>
|
||||
<string name="share">Share</string>
|
||||
<string name="new_node_seen">New Node Seen: %s</string>
|
||||
<string name="disconnected">Disconnected</string>
|
||||
<string name="device_sleeping">Device sleeping</string>
|
||||
<string name="connected_count">Connected: %1$s online</string>
|
||||
|
|
@ -435,6 +436,7 @@
|
|||
<string name="downlink_enabled">Downlink enabled</string>
|
||||
<string name="default_">Default</string>
|
||||
<string name="position_enabled">Position enabled</string>
|
||||
<string name="precise_location">Precise location</string>
|
||||
<string name="gpio_pin">GPIO pin</string>
|
||||
<string name="type">Type</string>
|
||||
<string name="hide_password">Hide password</string>
|
||||
|
|
@ -653,6 +655,7 @@
|
|||
<string name="firmware_version">Firmware version</string>
|
||||
<string name="timestamp">Timestamp</string>
|
||||
<string name="heading">Heading</string>
|
||||
<string name="speed">Speed</string>
|
||||
<string name="sats">Sats</string>
|
||||
<string name="alt">Alt</string>
|
||||
<string name="freq">Freq</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue