diff --git a/android-application/build.gradle.kts b/android-application/build.gradle.kts
index 55f22a4..3fec3d4 100644
--- a/android-application/build.gradle.kts
+++ b/android-application/build.gradle.kts
@@ -15,11 +15,11 @@ if (useGoogleServices) {
}
android {
- compileSdk = 34
+ compileSdk = 35
namespace = "com.inkapplications.ack.android"
defaultConfig {
minSdk = 21
- targetSdk = 34
+ targetSdk = 35
multiDexEnabled = true
buildConfigField("boolean", "USE_GOOGLE_SERVICES", useGoogleServices.toString())
buildConfigField("String", "COMMIT", optionalStringProperty("commit").buildQuote())
diff --git a/android-application/src/main/AndroidManifest.xml b/android-application/src/main/AndroidManifest.xml
index 12fdefb..5717fed 100644
--- a/android-application/src/main/AndroidManifest.xml
+++ b/android-application/src/main/AndroidManifest.xml
@@ -3,6 +3,9 @@
+
+
+
@@ -33,7 +36,8 @@
-
+
+
= Build.VERSION_CODES.Q) {
+ val hasBackgroundLocation = checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED
+
+ if (!hasBackgroundLocation) {
+ AlertDialog.Builder(this)
+ .setTitle("Background Location Permission Required")
+ .setMessage("To capture data in the background, this app needs access to your location even when the app is closed.\n\nPlease grant \"Allow all the time\" location permission in the application settings.")
+ .setPositiveButton("Open Settings") { _, _ ->
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+ data = Uri.fromParts("package", packageName, null)
+ }
+ startActivity(intent)
+ }
+ .setNegativeButton("Cancel", null)
+ .show()
+ return
+ }
+ }
+
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
permissionGate.withPermissions(*captureEvents.getDriverConnectPermissions().toTypedArray()) {
lifecycleScope.launch {
- if (captureEvents.driverSelection.first() == DriverSelection.Tnc) {
- startConnectTncActivity(backgroundCaptureServiceIntent)
- } else {
- startService(backgroundCaptureServiceIntent)
+ when (captureEvents.driverSelection.first()) {
+ DriverSelection.Tnc -> {
+ startConnectTncActivity(backgroundCaptureServiceIntent)
+ }
+ DriverSelection.Audio -> {
+ startForegroundService(backgroundCaptureAudioServiceIntent)
+ }
+ else -> {
+ startForegroundService(backgroundCaptureServiceIntent)
+ }
}
}
}
diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureScreen.kt
index 4a3939c..5962bd3 100644
--- a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureScreen.kt
+++ b/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureScreen.kt
@@ -62,6 +62,7 @@ fun CaptureScreen(
) {
Column {
Scaffold(
+ modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars),
bottomBar = { CaptureBottomBar(navController) },
floatingActionButton = { CaptureSettingsFab(settingsSheetState) },
isFloatingActionButtonDocked = true,
@@ -197,7 +198,10 @@ private fun CaptureSettingsSheet(
controlPanelState: ControlPanelState,
captureController: CaptureNavController,
settingsSheetState: BottomSheetScaffoldState,
-) = Column(horizontalAlignment = Alignment.CenterHorizontally) {
+) = Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars),
+) {
val scope = rememberCoroutineScope()
BackHandler(enabled = settingsSheetState.bottomSheetState.isExpanded) {
scope.launch { settingsSheetState.bottomSheetState.collapse() }
diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/service/BackgroundCaptureService.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/service/BackgroundCaptureService.kt
index 3b91346..fe04f56 100644
--- a/android-application/src/main/java/com/inkapplications/ack/android/capture/service/BackgroundCaptureService.kt
+++ b/android-application/src/main/java/com/inkapplications/ack/android/capture/service/BackgroundCaptureService.kt
@@ -14,7 +14,7 @@ import javax.inject.Inject
* Service that runs in the background to collect and transmit packets.
*/
@AndroidEntryPoint
-class BackgroundCaptureService: Service() {
+open class BackgroundCaptureService: Service() {
private lateinit var runScope: CoroutineScope
@Inject
@@ -51,3 +51,5 @@ class BackgroundCaptureService: Service() {
super.onDestroy()
}
}
+
+class BackgroundCaptureServiceAudio: BackgroundCaptureService()
diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsScreen.kt
index e73af9a..3bcf330 100644
--- a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsScreen.kt
+++ b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsScreen.kt
@@ -55,13 +55,21 @@ private fun Details(
interactive = false,
modifier = Modifier.aspectRatio(16f / 9f),
)
- IconButton(
- onClick = controller::onBackPressed
+ Box(
+ modifier = Modifier
+ .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
) {
- Icon(Icons.Default.ArrowBack, stringResource(R.string.navigate_up))
+ IconButton(
+ onClick = controller::onBackPressed
+ ) {
+ Icon(Icons.Default.ArrowBack, stringResource(R.string.navigate_up))
+ }
}
}
- Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = AckTheme.spacing.gutter)) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(top = AckTheme.spacing.gutter)
+ ) {
Text(
viewState.name,
style = AckTheme.typography.h1,
@@ -78,11 +86,15 @@ private fun Details(
}
} else {
Box(
- Modifier.padding(
- start = AckTheme.spacing.gutter,
- top = AckTheme.spacing.gutter,
- end = AckTheme.spacing.gutter,
- )
+ Modifier
+ .padding(
+ top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(),
+ )
+ .padding(
+ start = AckTheme.spacing.gutter,
+ top = AckTheme.spacing.gutter,
+ end = AckTheme.spacing.gutter,
+ )
) {
NavigationRow(
title = {
@@ -104,12 +116,16 @@ private fun Details(
}
}
Column(
- modifier = Modifier.padding(
- top = AckTheme.spacing.content,
- start = AckTheme.spacing.gutter,
- end = AckTheme.spacing.gutter,
- bottom = AckTheme.spacing.gutter,
- ),
+ modifier = Modifier
+ .padding(
+ bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
+ )
+ .padding(
+ top = AckTheme.spacing.content,
+ start = AckTheme.spacing.gutter,
+ end = AckTheme.spacing.gutter,
+ bottom = AckTheme.spacing.gutter,
+ ),
) {
IconRow(
icon = viewState.receiveIcon,
diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MapScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MapScreen.kt
index e0ca480..edde0b9 100644
--- a/android-application/src/main/java/com/inkapplications/ack/android/map/MapScreen.kt
+++ b/android-application/src/main/java/com/inkapplications/ack/android/map/MapScreen.kt
@@ -1,9 +1,6 @@
package com.inkapplications.ack.android.map
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.*
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.contentColorFor
@@ -40,7 +37,11 @@ fun MapScreen(
)
val logState = state.selectedItem
if (state.selectedItemVisible && logState != null) {
- Row (modifier = Modifier.padding(top = AckTheme.spacing.gutter)) {
+ Row (
+ modifier = Modifier
+ .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
+ .padding(top = AckTheme.spacing.gutter)
+ ) {
AprsLogItem(
log = logState,
onClick = onLogItemClick,
diff --git a/android-application/src/main/java/com/inkapplications/ack/android/onboard/UsageAgreementPrompt.kt b/android-application/src/main/java/com/inkapplications/ack/android/onboard/UsageAgreementPrompt.kt
index 0d631e5..dccef75 100644
--- a/android-application/src/main/java/com/inkapplications/ack/android/onboard/UsageAgreementPrompt.kt
+++ b/android-application/src/main/java/com/inkapplications/ack/android/onboard/UsageAgreementPrompt.kt
@@ -18,7 +18,12 @@ fun UsageAgreementPrompt(
controller: UserAgreementController,
) {
Column(
- modifier = Modifier.padding(AckTheme.spacing.gutter)
+ modifier = Modifier
+ .padding(
+ top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(),
+ bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
+ )
+ .padding(AckTheme.spacing.gutter)
.fillMaxHeight()
.verticalScroll(rememberScrollState()),
) {
diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsScreen.kt
index 8539dbf..4e3bcf7 100644
--- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsScreen.kt
+++ b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsScreen.kt
@@ -27,7 +27,13 @@ fun SettingsScreen(
controller: SettingsController,
) = AckScreen {
Column(
- modifier = Modifier.verticalScroll(rememberScrollState()),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(
+ top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(),
+ bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
+ )
+ .verticalScroll(rememberScrollState()),
) {
NavigationRow(
title = stringResource(R.string.settings_title),
diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicensePrompt.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicensePrompt.kt
index 130f342..cb1c3b8 100644
--- a/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicensePrompt.kt
+++ b/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicensePrompt.kt
@@ -20,7 +20,12 @@ fun LicensePromptScreen(
onContinue: (LicensePromptFieldValues) -> Unit,
) = AckScreen {
Column (
- modifier = Modifier.padding(AckTheme.spacing.gutter),
+ modifier = Modifier
+ .padding(
+ top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(),
+ bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
+ )
+ .padding(AckTheme.spacing.gutter),
) {
Text(
"License Info",
diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationScreen.kt
index 72c37b1..69fac28 100644
--- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationScreen.kt
+++ b/android-application/src/main/java/com/inkapplications/ack/android/station/StationScreen.kt
@@ -50,13 +50,21 @@ private fun StationDetails(
interactive = false,
modifier = Modifier.aspectRatio(16f / 9f),
)
- IconButton(
- onClick = controller::onBackPressed
+ Box(
+ modifier = Modifier
+ .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
) {
- Icon(Icons.Default.ArrowBack, stringResource(R.string.navigate_up))
+ IconButton(
+ onClick = controller::onBackPressed
+ ) {
+ Icon(Icons.Default.ArrowBack, stringResource(R.string.navigate_up))
+ }
}
}
- Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = AckTheme.spacing.gutter)) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(top = AckTheme.spacing.gutter)
+ ) {
Text(
viewState.insight.name,
style = AckTheme.typography.h1,
@@ -69,11 +77,15 @@ private fun StationDetails(
}
} else {
Box(
- Modifier.padding(
- start = AckTheme.spacing.gutter,
- top = AckTheme.spacing.gutter,
- end = AckTheme.spacing.gutter,
- )
+ Modifier
+ .padding(
+ top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(),
+ )
+ .padding(
+ start = AckTheme.spacing.gutter,
+ top = AckTheme.spacing.gutter,
+ end = AckTheme.spacing.gutter,
+ )
) {
NavigationRow(
title = {
@@ -91,12 +103,16 @@ private fun StationDetails(
}
}
Column(
- modifier = Modifier.padding(
- top = AckTheme.spacing.content,
- start = AckTheme.spacing.gutter,
- end = AckTheme.spacing.gutter,
- bottom = AckTheme.spacing.gutter,
- ),
+ modifier = Modifier
+ .padding(
+ bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
+ )
+ .padding(
+ top = AckTheme.spacing.content,
+ start = AckTheme.spacing.gutter,
+ end = AckTheme.spacing.gutter,
+ bottom = AckTheme.spacing.gutter,
+ ),
) {
if (viewState.insight.temperature != null) {
IconRow(Icons.Default.WbSunny, viewState.insight.temperature)
diff --git a/android-application/src/main/java/com/inkapplications/ack/android/symbol/AndroidSymbolFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/symbol/AndroidSymbolFactory.kt
index 60de487..76a5a0e 100644
--- a/android-application/src/main/java/com/inkapplications/ack/android/symbol/AndroidSymbolFactory.kt
+++ b/android-application/src/main/java/com/inkapplications/ack/android/symbol/AndroidSymbolFactory.kt
@@ -37,7 +37,7 @@ class AndroidSymbolFactory @Inject constructor(
}
private fun Bitmap.withOverlay(other: Bitmap): Bitmap {
- val bmOverlay = Bitmap.createBitmap(width, height, config)
+ val bmOverlay = Bitmap.createBitmap(width, height, config ?: Bitmap.Config.ARGB_8888)
val canvas = Canvas(bmOverlay)
canvas.drawBitmap(this, Matrix(), null)
canvas.drawBitmap(other, 0f, 0f, null)
diff --git a/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncScreen.kt
index e774c7b..cbd5acc 100644
--- a/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncScreen.kt
+++ b/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncScreen.kt
@@ -33,6 +33,10 @@ fun DeviceList(
is ConnectTncState.Discovering.Empty -> {
Box(
modifier = Modifier.fillMaxSize()
+ .padding(
+ top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(),
+ bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
+ ),
) {
DeviceHeader(controller::onCloseClick)
Column(
diff --git a/aprs-android/build.gradle.kts b/aprs-android/build.gradle.kts
index 59e952e..f20697d 100644
--- a/aprs-android/build.gradle.kts
+++ b/aprs-android/build.gradle.kts
@@ -15,7 +15,7 @@ sqldelight {
android {
namespace = "com.inkapplications.ack.data"
- compileSdk = 33
+ compileSdk = 34
ndkVersion = "21.3.6528147"
defaultConfig {
diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/AfskDriver.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/AfskDriver.kt
index 3780b28..cba9778 100644
--- a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/AfskDriver.kt
+++ b/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/AfskDriver.kt
@@ -35,12 +35,24 @@ class AfskDriver internal constructor(
}
override val incoming = MutableSharedFlow()
override val receivePermissions: Set = when {
-// Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> setOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.POST_NOTIFICATIONS)
+ Build.VERSION.SDK_INT >= 34 -> setOf(
+ Manifest.permission.RECORD_AUDIO,
+ Manifest.permission.FOREGROUND_SERVICE_MICROPHONE,
+ // TODO: This could be simplified/removed if we create separate services for transmit and receive.
+ // Leaving for now, since all the other drivers require it anyway.
+ Manifest.permission.FOREGROUND_SERVICE_LOCATION,
+ Manifest.permission.POST_NOTIFICATIONS,
+ )
+ Build.VERSION.SDK_INT >= 33 -> setOf(
+ Manifest.permission.RECORD_AUDIO,
+ Manifest.permission.POST_NOTIFICATIONS,
+ )
else -> setOf(Manifest.permission.RECORD_AUDIO)
}
override val transmitPermissions: Set = when {
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> setOf(Manifest.permission.POST_NOTIFICATIONS)
+ Build.VERSION.SDK_INT >= 33 -> setOf(
+ Manifest.permission.POST_NOTIFICATIONS
+ )
else -> emptySet()
}
override val volume = audioProcessor.volume
diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/InternetDriver.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/InternetDriver.kt
index 4afb0b3..579a685 100644
--- a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/InternetDriver.kt
+++ b/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/InternetDriver.kt
@@ -32,11 +32,21 @@ class InternetDriver internal constructor(
override val connectionState: Flow = clientConnectionState
override val incoming = MutableSharedFlow()
override val receivePermissions: Set = when {
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> setOf(Manifest.permission.POST_NOTIFICATIONS, Manifest.permission.ACCESS_FINE_LOCATION)
+ Build.VERSION.SDK_INT >= 34 -> setOf(
+ Manifest.permission.POST_NOTIFICATIONS,
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.FOREGROUND_SERVICE_LOCATION,
+ )
+ Build.VERSION.SDK_INT >= 33 -> setOf(
+ Manifest.permission.POST_NOTIFICATIONS,
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ )
else -> setOf(Manifest.permission.ACCESS_FINE_LOCATION)
}
override val transmitPermissions: Set = when {
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> setOf(Manifest.permission.POST_NOTIFICATIONS)
+ Build.VERSION.SDK_INT >= 33 -> setOf(
+ Manifest.permission.POST_NOTIFICATIONS,
+ )
else -> emptySet()
}
private val transmitQueue = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/TncDriver.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/TncDriver.kt
index 585296c..83836ca 100644
--- a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/TncDriver.kt
+++ b/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/TncDriver.kt
@@ -49,11 +49,31 @@ class TncDriver internal constructor(
}
}
override val receivePermissions: Set = when {
- Build.VERSION.SDK_INT > Build.VERSION_CODES.S -> setOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN)
+ Build.VERSION.SDK_INT >= 34 -> setOf(
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.FOREGROUND_SERVICE_LOCATION,
+ Manifest.permission.BLUETOOTH_CONNECT,
+ Manifest.permission.BLUETOOTH_SCAN,
+ )
+ Build.VERSION.SDK_INT > 31 -> setOf(
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.BLUETOOTH_CONNECT,
+ Manifest.permission.BLUETOOTH_SCAN,
+ )
else -> setOf(Manifest.permission.ACCESS_FINE_LOCATION)
}
override val transmitPermissions: Set = when {
- Build.VERSION.SDK_INT > Build.VERSION_CODES.S -> setOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN)
+ Build.VERSION.SDK_INT >= 34 -> setOf(
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.FOREGROUND_SERVICE_LOCATION,
+ Manifest.permission.BLUETOOTH_CONNECT,
+ Manifest.permission.BLUETOOTH_SCAN
+ )
+ Build.VERSION.SDK_INT > 31 -> setOf(
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.BLUETOOTH_CONNECT,
+ Manifest.permission.BLUETOOTH_SCAN
+ )
else -> setOf(Manifest.permission.ACCESS_FINE_LOCATION)
}
private val outputStream = MutableStateFlow(null)