Skip to content
Draft
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
11 changes: 2 additions & 9 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,10 @@
android:name="android.hardware.location.gps"
android:required="false" />

<!-- Request legacy Bluetooth permissions on older devices -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />

<!-- API 31+ Bluetooth permissions -->
<!-- API 31+ Bluetooth permissions (Min SDK is 32) -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
android:usesPermissionFlags="neverForLocation" />

<!-- API 33+ Notification runtime permissions -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
Expand Down
12 changes: 3 additions & 9 deletions app/src/main/java/com/geeksville/mesh/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import android.content.Intent
import android.graphics.Color
import android.hardware.usb.UsbManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
Expand Down Expand Up @@ -67,14 +66,9 @@ class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
enableEdgeToEdge(
// Disable three-button navbar scrim on pre-Q devices
navigationBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Disable three-button navbar scrim
window.setNavigationBarContrastEnforced(false)
}
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT))
// Disable three-button navbar scrim (unconditional on API 32+)
window.setNavigationBarContrastEnforced(false)

super.onCreate(savedInstanceState)

Expand Down
15 changes: 5 additions & 10 deletions app/src/main/java/com/geeksville/mesh/service/MeshService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import android.os.RemoteException
import android.util.Log
Expand Down Expand Up @@ -477,14 +476,10 @@ class MeshService : Service() {
this,
SERVICE_NOTIFY_ID,
notification,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (hasLocationPermission()) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
} else {
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
}
if (hasLocationPermission()) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
} else {
0 // No specific type needed for older Android versions
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
},
)
} catch (ex: Exception) {
Expand Down Expand Up @@ -1359,7 +1354,7 @@ class MeshService : Service() {
val failure =
when {
address == null -> "no_active_address"
myNodeNum == null -> "no_my_node"
myNodeInfo == null -> "no_my_node"
else -> null
}
if (failure != null) {
Expand All @@ -1368,7 +1363,7 @@ class MeshService : Service() {
}

val safeAddress = address!!
val myNum = myNodeNum!!
val myNum = myNodeNum
val storeForwardConfig = moduleConfig.storeForward
val lastRequest = meshPrefs.getStoreForwardLastRequest(safeAddress)
val (window, max) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package com.geeksville.mesh.service

import android.app.ForegroundServiceStartNotAllowedException
import android.content.Context
import android.os.Build
import co.touchlab.kermit.Logger
import com.geeksville.mesh.BuildConfig

Expand All @@ -36,13 +35,9 @@ fun MeshService.Companion.startService(context: Context) {
Logger.i { "Trying to start service debug=${BuildConfig.DEBUG}" }

val intent = createIntent(context)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
try {
context.startForegroundService(intent)
} catch (ex: ForegroundServiceStartNotAllowedException) {
Logger.e { "Unable to start service: ${ex.message}" }
}
} else {
try {
context.startForegroundService(intent)
} catch (ex: ForegroundServiceStartNotAllowedException) {
Logger.e { "Unable to start service: ${ex.message}" }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
package com.geeksville.mesh.ui.connections

import android.net.InetAddresses
import android.os.Build
import android.util.Patterns
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand Down Expand Up @@ -93,12 +91,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.radio.component.PacketResponseStateDialog
import org.meshtastic.proto.ConfigProtos

fun String?.isIPAddress(): Boolean = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@Suppress("DEPRECATION")
this != null && Patterns.IP_ADDRESS.matcher(this).matches()
} else {
InetAddresses.isNumericAddress(this.toString())
}
fun String?.isIPAddress(): Boolean = InetAddresses.isNumericAddress(this.toString())

/**
* Composable screen for managing device connections (BLE, TCP, USB). It handles permission requests for location and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package com.geeksville.mesh.ui.connections.components

import android.Manifest
import android.content.Intent
import android.os.Build
import android.provider.Settings.ACTION_BLUETOOTH_SETTINGS
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
Expand All @@ -40,7 +39,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
Expand All @@ -52,8 +50,6 @@ import com.geeksville.mesh.model.DeviceListEntry
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.Res
Expand All @@ -63,11 +59,9 @@ import org.meshtastic.core.strings.bluetooth_paired_devices
import org.meshtastic.core.strings.grant_permissions
import org.meshtastic.core.strings.no_ble_devices
import org.meshtastic.core.strings.open_settings
import org.meshtastic.core.strings.permission_missing
import org.meshtastic.core.strings.permission_missing_31
import org.meshtastic.core.strings.scan
import org.meshtastic.core.strings.scanning_bluetooth
import org.meshtastic.core.ui.util.showToast

/**
* Composable that displays a list of Bluetooth Low Energy (BLE) devices and allows scanning. It handles Bluetooth
Expand All @@ -91,54 +85,21 @@ fun BLEDevices(
scanModel: BTScanModel,
bluetoothEnabled: Boolean,
) {
LocalContext.current // Used implicitly by stringResource
val isScanning by scanModel.spinner.collectAsStateWithLifecycle(false)

// Define permissions needed for Bluetooth scanning based on Android version.
// Bluetooth permissions for Android 12+ (Min SDK is 32)
val bluetoothPermissionsList = remember {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
listOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
} else {
listOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)
}
listOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
}

val context = LocalContext.current
val permsMissing =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
stringResource(Res.string.permission_missing_31)
} else {
stringResource(Res.string.permission_missing)
}
val coroutineScope = rememberCoroutineScope()

val singlePermissionState =
rememberPermissionState(
permission = Manifest.permission.ACCESS_BACKGROUND_LOCATION,
onPermissionResult = { granted ->
scanModel.refreshPermissions()
scanModel.startScan()
},
)

val permissionsState =
rememberMultiplePermissionsState(
permissions = bluetoothPermissionsList,
onPermissionsResult = { permissions ->
val granted = permissions.values.all { it }
if (permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false)) {
coroutineScope.launch { context.showToast(permsMissing) }
singlePermissionState.launchPermissionRequest()
}
if (granted) {
scanModel.refreshPermissions()
scanModel.startScan()
} else {
coroutineScope.launch { context.showToast(permsMissing) }
}
},
)
Expand Down Expand Up @@ -231,12 +192,7 @@ fun BLEDevices(
} else {
// Show a message and a button to grant permissions if not all granted
EmptyStateContent(
text =
if (permissionsState.shouldShowRationale) {
stringResource(Res.string.permission_missing)
} else {
stringResource(Res.string.permission_missing_31)
},
text = stringResource(Res.string.permission_missing_31),
actionButton = {
Button(onClick = { checkPermissionsAndScan(permissionsState, scanModel, bluetoothEnabled) }) {
Text(text = stringResource(Res.string.grant_permissions))
Expand Down
2 changes: 1 addition & 1 deletion config.properties
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ VERSION_CODE_OFFSET=29314197

# Application and SDK versions
APPLICATION_ID=com.geeksville.mesh
MIN_SDK=26
MIN_SDK=32
TARGET_SDK=36
COMPILE_SDK=36

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.LocationManager
import android.os.Build
import androidx.core.content.ContextCompat

/** Checks if the device has a GPS receiver. */
Expand All @@ -44,22 +43,12 @@ fun Context.gpsDisabled(): Boolean {
* Determines the list of Bluetooth permissions that are currently missing. Internal helper for
* [hasBluetoothPermission].
*
* For Android S (API 31) and above, this includes [Manifest.permission.BLUETOOTH_SCAN] and
* [Manifest.permission.BLUETOOTH_CONNECT]. For older versions, it includes [Manifest.permission.ACCESS_FINE_LOCATION]
* as it is required for Bluetooth scanning.
* This includes [Manifest.permission.BLUETOOTH_SCAN] and [Manifest.permission.BLUETOOTH_CONNECT].
*
* @return Array of missing Bluetooth permission strings. Empty if all are granted.
*/
private fun Context.getBluetoothPermissions(): Array<String> {
val requiredPermissions = mutableListOf<String>()

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requiredPermissions.add(Manifest.permission.BLUETOOTH_SCAN)
requiredPermissions.add(Manifest.permission.BLUETOOTH_CONNECT)
} else {
// ACCESS_FINE_LOCATION is required for Bluetooth scanning on pre-S devices.
requiredPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION)
}
val requiredPermissions = listOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
return requiredPermissions
.filter { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED }
.toTypedArray()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ constructor(

val providerList = buildList {
val providers = allProviders
if (android.os.Build.VERSION.SDK_INT >= 31 && LocationManager.FUSED_PROVIDER in providers) {
if (LocationManager.FUSED_PROVIDER in providers) {
add(LocationManager.FUSED_PROVIDER)
} else {
if (LocationManager.GPS_PROVIDER in providers) add(LocationManager.GPS_PROVIDER)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,9 @@ enum class DistanceUnit(val symbol: String, val multiplier: Float, val system: I

companion object {
fun getFromLocale(locale: Locale = Locale.getDefault()): DisplayUnits =
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
when (LocaleData.getMeasurementSystem(ULocale.forLocale(locale))) {
LocaleData.MeasurementSystem.SI -> DisplayUnits.METRIC
else -> DisplayUnits.IMPERIAL
}
} else {
when (locale.country.uppercase(locale)) {
"US",
"LR",
"MM",
"GB",
-> DisplayUnits.IMPERIAL
else -> DisplayUnits.METRIC
}
when (LocaleData.getMeasurementSystem(ULocale.forLocale(locale))) {
LocaleData.MeasurementSystem.SI -> DisplayUnits.METRIC
else -> DisplayUnits.IMPERIAL
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

package org.meshtastic.core.ui.theme

import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme
Expand Down Expand Up @@ -283,7 +282,7 @@ fun AppTheme(
) {
val colorScheme =
when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
dynamicColor -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
Expand Down
Loading