Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 30 additions & 11 deletions app/src/main/java/app/fieldwatch/alert/Alerter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ import androidx.core.app.NotificationCompat
import app.fieldwatch.MainActivity
import app.fieldwatch.R
import app.fieldwatch.domain.AlertVoiceWhat
import app.fieldwatch.domain.AppLanguage
import app.fieldwatch.domain.Fleet
import app.fieldwatch.domain.MacUtil
import app.fieldwatch.domain.RadioBookmarks
import app.fieldwatch.domain.Sighting
import app.fieldwatch.domain.label
import app.fieldwatch.domain.WatchTarget
import app.fieldwatch.domain.spokenWatchPhrase
import app.fieldwatch.domain.testWatchPhrase
import app.fieldwatch.ui.translateAppText
import java.util.Locale
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
Expand All @@ -47,6 +50,8 @@ class Alerter(private val context: Context) {
private var tts: TextToSpeech? = null
private var ttsReady = false
private var speaking = false
private var legacyChannelCleanupDone = false
private var channelLanguage: AppLanguage? = null
private var utteranceStarted = false
private var ttsSpeakTries = 0
private var pendingSpeak: String? = null
Expand All @@ -63,7 +68,7 @@ class Alerter(private val context: Context) {
val flashes: SharedFlow<String> = _flashes.asSharedFlow()

init {
ensureChannel()
ensureChannel(AppLanguage.ENGLISH)
}

fun checkLive(
Expand All @@ -78,11 +83,12 @@ class Alerter(private val context: Context) {
visibleOnLive: (Sighting) -> Boolean = { !it.gone },
arrivalsOnly: Boolean = false,
demoMode: Boolean = false,
language: AppLanguage = AppLanguage.ENGLISH,
) {
if (!alertsOn || watchlist.isEmpty()) return
fireNewAppearances(
devices, fleets, watchlist, beepOn, voiceOn, voiceWhat, shadeOn,
visibleOnLive, arrivalsOnly, demoMode,
visibleOnLive, arrivalsOnly, demoMode, language,
)
}

Expand Down Expand Up @@ -126,6 +132,7 @@ class Alerter(private val context: Context) {
visibleOnLive: (Sighting) -> Boolean,
arrivalsOnly: Boolean,
demoMode: Boolean,
language: AppLanguage,
) {
val now = System.currentTimeMillis()
val onAir = devices.filter { !it.gone }
Expand All @@ -151,7 +158,7 @@ class Alerter(private val context: Context) {
device.mac,
demoMode,
)
if (shadeOn && target.notify) notify(label, device, demoMode)
if (shadeOn && target.notify) notify(label, device, demoMode, language)
if (target.vibrate) vibrate()
if (beepOn && !beepFired) {
beep(holdFocus = !voiceOn)
Expand Down Expand Up @@ -182,7 +189,8 @@ class Alerter(private val context: Context) {
return now - spanStart <= NEW_WINDOW_MS
}

private fun notify(label: String, device: Sighting, demoMode: Boolean) {
private fun notify(label: String, device: Sighting, demoMode: Boolean, language: AppLanguage) {
ensureChannel(language)
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra(EXTRA_DEVICE_KEY, device.key)
Expand All @@ -195,11 +203,11 @@ class Alerter(private val context: Context) {
)
val note = NotificationCompat.Builder(context, CHANNEL)
.setSmallIcon(R.drawable.ic_stat_fieldwatch)
.setContentTitle("Fieldwatch watchlist")
.setContentTitle(translateAppText("Fieldwatch watchlist", language))
.setContentText("$label ${device.rssi} dBm ${MacUtil.screenMac(device.mac, demoMode)}")
.setStyle(
NotificationCompat.BigTextStyle().bigText(
"$label appeared\n${device.kind} ${MacUtil.screenMac(device.mac, demoMode)}\n${device.rssi} dBm ${MacUtil.redactMacIn(device.displayName, device.mac, demoMode)}",
"$label ${translateAppText("appeared", language)}\n${translateAppText(device.kind.label(), language)} ${MacUtil.screenMac(device.mac, demoMode)}\n${device.rssi} dBm ${MacUtil.redactMacIn(device.displayName, device.mac, demoMode)}",
),
)
.setPriority(NotificationCompat.PRIORITY_HIGH)
Expand Down Expand Up @@ -487,18 +495,29 @@ class Alerter(private val context: Context) {
vibrator.vibrate(VibrationEffect.createWaveform(longArrayOf(0, 80, 60, 80), -1))
}

private fun ensureChannel() {
private fun ensureChannel(language: AppLanguage) {
if (Build.VERSION.SDK_INT < 26) return
runCatching { manager.deleteNotificationChannel("fieldwatch_watch_v2") }
if (manager.getNotificationChannel(CHANNEL) != null) return
if (!legacyChannelCleanupDone) {
runCatching { manager.deleteNotificationChannel("fieldwatch_watch_v2") }
legacyChannelCleanupDone = true
}
if (channelLanguage == language) return
manager.createNotificationChannel(
NotificationChannel(CHANNEL, "Watchlist", NotificationManager.IMPORTANCE_HIGH).apply {
description = "Appearing signatures and devices on your watchlist. Beep is played separately."
NotificationChannel(
CHANNEL,
translateAppText("Watchlist alerts", language),
NotificationManager.IMPORTANCE_HIGH,
).apply {
description = translateAppText(
"Appearing signatures and devices on your watchlist. Beep is played separately.",
language,
)
enableVibration(true)
enableLights(true)
setSound(null, null)
},
)
channelLanguage = language
}

companion object {
Expand Down
21 changes: 10 additions & 11 deletions app/src/main/java/app/fieldwatch/data/ConfigStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File

internal fun restoreConfigDefaults(current: PersistedConfig, defaults: PersistedConfig): PersistedConfig =
defaults.copy(
settings = defaults.settings.copy(
disclaimerAccepted = current.settings.disclaimerAccepted,
disclaimerRev = current.settings.disclaimerRev,
language = current.settings.language,
),
)

class ConfigStore(context: Context) {
private val file = File(context.filesDir, "config.json")
private val json = Json {
Expand Down Expand Up @@ -92,17 +101,7 @@ class ConfigStore(context: Context) {
}

suspend fun restoreDefaults() {
val accepted = _config.value.settings.disclaimerAccepted
val rev = _config.value.settings.disclaimerRev
update {
val fresh = seed()
fresh.copy(
settings = fresh.settings.copy(
disclaimerAccepted = accepted,
disclaimerRev = rev,
),
)
}
update { current -> restoreConfigDefaults(current, seed()) }
}

suspend fun importFleets(incoming: List<Fleet>): SignatureImportResult = mutex.withLock {
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/app/fieldwatch/domain/Models.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ enum class ScanIntensity { SAVER, BALANCED, PERFORMANCE }
@Serializable
enum class LogFormat { CSV, JSON }

@Serializable
enum class AppLanguage {
ENGLISH,
SPANISH,
}

@Serializable
enum class FilterLogic { AND, OR }

Expand Down Expand Up @@ -648,6 +654,8 @@ data class AppSettings(
val signatureListSort: SignatureListSort = SignatureListSort.NAME,
/** By class: hide class headers with 0 radios. Off = show all (zeros stay). */
val outlineHideEmpty: Boolean = false,
/** Language used by the app UI. Missing from older configs means English. */
val language: AppLanguage = AppLanguage.ENGLISH,
)

const val DISCLAIMER_REV = 3
Expand Down
37 changes: 28 additions & 9 deletions app/src/main/java/app/fieldwatch/radio/ScanService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import app.fieldwatch.domain.Observation
import app.fieldwatch.domain.RadioKind
import app.fieldwatch.domain.ScanIntensity
import app.fieldwatch.domain.detectionPolicy
import app.fieldwatch.ui.translateAppText
import android.os.SystemClock
import android.util.Log
import kotlinx.coroutines.CancellationException
Expand All @@ -37,6 +38,7 @@ class ScanService : LifecycleService() {
private var bleStartJob: Job? = null
private var lastIntensity: ScanIntensity? = null
private var lastNotifAt = 0L
private var channelLanguage: app.fieldwatch.domain.AppLanguage? = null
private val inbound = Channel<Observation>(512, BufferOverflow.DROP_OLDEST)
private val publishGate = Any()
private var publishJob: Job? = null
Expand All @@ -45,8 +47,8 @@ class ScanService : LifecycleService() {

override fun onCreate() {
super.onCreate()
ensureChannel()
val app = application as FieldwatchApp
ensureChannel(app.config.settings.language)
wifi = WifiRadio(
this,
onObservation = { offer(it) },
Expand Down Expand Up @@ -256,6 +258,7 @@ class ScanService : LifecycleService() {
visibleOnLive = app::wouldShowOnLive,
arrivalsOnly = app.config.filter.arrivalsOnly,
demoMode = settings.demoMode,
language = settings.language,
)
}
app.tak.publish(
Expand All @@ -273,7 +276,10 @@ class ScanService : LifecycleService() {
}

private fun startAsForeground() {
val notification = buildNotification("Starting radios…")
val app = application as FieldwatchApp
val language = app.config.settings.language
ensureChannel(language)
val notification = buildNotification(translateAppText("Starting radios…", language))
if (Build.VERSION.SDK_INT >= 34) {
startForeground(
NOTIF_ID,
Expand All @@ -296,13 +302,20 @@ class ScanService : LifecycleService() {
val now = System.currentTimeMillis()
if (now - lastNotifAt < 2_500L) return
lastNotifAt = now
val stats = (application as FieldwatchApp).devices.stats.value
val text = "${stats.wifiNow} Wi-Fi · ${stats.bleNow} BLE · ${stats.namedNow} signatures"
val app = application as FieldwatchApp
val language = app.config.settings.language
ensureChannel(language)
val stats = app.devices.stats.value
val text = translateAppText(
"${stats.wifiNow} Wi-Fi · ${stats.bleNow} BLE · ${stats.namedNow} signatures",
language,
)
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
nm.notify(NOTIF_ID, buildNotification(text))
}

private fun buildNotification(text: String): Notification {
val language = (application as FieldwatchApp).config.settings.language
val launch = PendingIntent.getActivity(
this,
0,
Expand All @@ -317,12 +330,12 @@ class ScanService : LifecycleService() {
)
return NotificationCompat.Builder(this, CHANNEL)
.setSmallIcon(R.drawable.ic_stat_fieldwatch)
.setContentTitle("Fieldwatch scanning")
.setContentTitle(translateAppText("Fieldwatch scanning", language))
.setContentText(text)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setContentIntent(launch)
.addAction(0, "Stop", stop)
.addAction(0, translateAppText("Stop", language), stop)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.build()
}
Expand Down Expand Up @@ -373,15 +386,21 @@ class ScanService : LifecycleService() {
return null
}

private fun ensureChannel() {
private fun ensureChannel(language: app.fieldwatch.domain.AppLanguage) {
if (Build.VERSION.SDK_INT < 26) return
if (channelLanguage == language) return
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
nm.createNotificationChannel(
NotificationChannel(CHANNEL, "Scanning", NotificationManager.IMPORTANCE_LOW).apply {
description = "Passive Wi-Fi and Bluetooth scan status"
NotificationChannel(
CHANNEL,
translateAppText("Scanning", language),
NotificationManager.IMPORTANCE_LOW,
).apply {
description = translateAppText("Passive Wi-Fi and Bluetooth scan status", language)
setShowBadge(false)
},
)
channelLanguage = language
}

companion object {
Expand Down
Loading