Fix/foreground permissions scanning (#556)

* try catch the foreground location service if not available

* ask for background permissions

* background permissions in onboarding

* small improvements
This commit is contained in:
callebtc
2026-01-05 15:09:04 +07:00
committed by GitHub
parent 9733806610
commit 423feb8b77
11 changed files with 481 additions and 53 deletions
+4 -1
View File
@@ -16,6 +16,7 @@
<!-- Location permission required for BLE scanning -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Notification permissions -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@@ -27,6 +28,8 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<!-- Data sync foreground service type (required when declaring dataSync) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- Location foreground service type required for BLE scanning in background -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Microphone for voice notes -->
@@ -104,7 +107,7 @@
<service
android:name=".service.MeshForegroundService"
android:exported="false"
android:foregroundServiceType="connectedDevice|dataSync"
android:foregroundServiceType="connectedDevice|dataSync|location"
tools:ignore="DataExtractionRules">
</service>
@@ -26,6 +26,7 @@ import com.bitchat.android.onboarding.BatteryOptimizationManager
import com.bitchat.android.onboarding.BatteryOptimizationPreferenceManager
import com.bitchat.android.onboarding.BatteryOptimizationScreen
import com.bitchat.android.onboarding.BatteryOptimizationStatus
import com.bitchat.android.onboarding.BackgroundLocationPermissionScreen
import com.bitchat.android.onboarding.InitializationErrorScreen
import com.bitchat.android.onboarding.InitializingScreen
import com.bitchat.android.onboarding.LocationCheckScreen
@@ -135,6 +136,9 @@ class MainActivity : OrientationAwareActivity() {
activity = this,
permissionManager = permissionManager,
onOnboardingComplete = ::handleOnboardingComplete,
onBackgroundLocationRequired = {
mainViewModel.updateOnboardingState(OnboardingState.BACKGROUND_LOCATION_EXPLANATION)
},
onOnboardingFailed = ::handleOnboardingFailed
)
@@ -267,6 +271,21 @@ class MainActivity : OrientationAwareActivity() {
)
}
OnboardingState.BACKGROUND_LOCATION_EXPLANATION -> {
BackgroundLocationPermissionScreen(
modifier = modifier,
onContinue = {
onboardingCoordinator.requestBackgroundLocation()
},
onRetry = {
onboardingCoordinator.checkBackgroundLocationAndProceed()
},
onSkip = {
onboardingCoordinator.skipBackgroundLocation()
}
)
}
OnboardingState.CHECKING, OnboardingState.INITIALIZING, OnboardingState.COMPLETE -> {
// Set up back navigation handling for the chat screen
val backCallback = object : OnBackPressedCallback(true) {
@@ -380,10 +399,17 @@ class MainActivity : OrientationAwareActivity() {
if (permissionManager.isFirstTimeLaunch()) {
Log.d("MainActivity", "First time launch, showing permission explanation")
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
} else if (permissionManager.areAllPermissionsGranted()) {
Log.d("MainActivity", "Existing user with permissions, initializing app")
} else if (permissionManager.areRequiredPermissionsGranted()) {
Log.d("MainActivity", "Existing user with required permissions")
if (permissionManager.needsBackgroundLocationPermission() &&
!permissionManager.isBackgroundLocationGranted() &&
!com.bitchat.android.onboarding.BackgroundLocationPreferenceManager.isSkipped(this@MainActivity)
) {
mainViewModel.updateOnboardingState(OnboardingState.BACKGROUND_LOCATION_EXPLANATION)
} else {
mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING)
initializeApp()
}
} else {
Log.d("MainActivity", "Existing user missing permissions, showing explanation")
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
@@ -0,0 +1,251 @@
package com.bitchat.android.onboarding
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Security
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.res.stringResource
import com.bitchat.android.R
/**
* Explanation screen shown before requesting background location permission.
*/
@Composable
fun BackgroundLocationPermissionScreen(
modifier: Modifier,
onContinue: () -> Unit,
onRetry: () -> Unit,
onSkip: () -> Unit
) {
val colorScheme = MaterialTheme.colorScheme
val scrollState = rememberScrollState()
Box(modifier = modifier) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 24.dp)
.padding(bottom = 88.dp)
.verticalScroll(scrollState),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Spacer(modifier = Modifier.height(24.dp))
HeaderSection(colorScheme)
Surface(
modifier = Modifier.fillMaxWidth(),
color = colorScheme.surfaceVariant.copy(alpha = 0.25f),
shape = RoundedCornerShape(12.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = Icons.Filled.LocationOn,
contentDescription = stringResource(R.string.cd_location_services),
tint = colorScheme.primary,
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp)
)
Column {
Text(
text = stringResource(R.string.background_location_required_title),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = colorScheme.onBackground
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(R.string.background_location_explanation),
style = MaterialTheme.typography.bodySmall,
color = colorScheme.onBackground.copy(alpha = 0.8f)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.background_location_settings_tip),
style = MaterialTheme.typography.bodySmall.copy(
fontFamily = FontFamily.Monospace
),
color = colorScheme.onBackground.copy(alpha = 0.8f)
)
}
}
}
}
Surface(
modifier = Modifier.fillMaxWidth(),
color = colorScheme.surfaceVariant.copy(alpha = 0.25f),
shape = RoundedCornerShape(12.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = Icons.Filled.Security,
contentDescription = stringResource(R.string.cd_privacy_protected),
tint = colorScheme.primary,
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp)
)
Column {
Text(
text = stringResource(R.string.background_location_needs_for),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = colorScheme.onBackground
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(R.string.background_location_needs_bullets),
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
color = colorScheme.onBackground.copy(alpha = 0.8f)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.background_location_privacy_note),
style = MaterialTheme.typography.bodySmall.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium
),
color = colorScheme.onBackground
)
}
}
}
}
Spacer(modifier = Modifier.height(24.dp))
}
Surface(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth(),
color = colorScheme.surface,
shadowElevation = 8.dp
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = onContinue,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = colorScheme.primary
)
) {
Text(
text = stringResource(R.string.grant_background_location),
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
),
modifier = Modifier.padding(vertical = 4.dp)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
OutlinedButton(
onClick = onRetry,
modifier = Modifier.weight(1f)
) {
Text(
text = stringResource(R.string.check_again),
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace
)
)
}
TextButton(
onClick = onSkip,
modifier = Modifier.weight(1f)
) {
Text(
text = stringResource(R.string.battery_optimization_skip),
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace
)
)
}
}
}
}
}
}
@Composable
private fun HeaderSection(colorScheme: ColorScheme) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.headlineLarge.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
fontSize = 32.sp
),
color = colorScheme.onBackground
)
Text(
text = stringResource(R.string.background_location_required_subtitle),
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onBackground.copy(alpha = 0.7f)
)
}
}
@@ -0,0 +1,21 @@
package com.bitchat.android.onboarding
import android.content.Context
/**
* Preference manager for background location skip choice.
*/
object BackgroundLocationPreferenceManager {
private const val PREFS_NAME = "bitchat_settings"
private const val KEY_BACKGROUND_LOCATION_SKIP = "background_location_skipped"
fun setSkipped(context: Context, skipped: Boolean) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().putBoolean(KEY_BACKGROUND_LOCATION_SKIP, skipped).apply()
}
fun isSkipped(context: Context): Boolean {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getBoolean(KEY_BACKGROUND_LOCATION_SKIP, false)
}
}
@@ -19,6 +19,7 @@ class OnboardingCoordinator(
private val activity: ComponentActivity,
private val permissionManager: PermissionManager,
private val onOnboardingComplete: () -> Unit,
private val onBackgroundLocationRequired: () -> Unit,
private val onOnboardingFailed: (String) -> Unit
) {
@@ -27,9 +28,11 @@ class OnboardingCoordinator(
}
private var permissionLauncher: ActivityResultLauncher<Array<String>>? = null
private var backgroundLocationLauncher: ActivityResultLauncher<String>? = null
init {
setupPermissionLauncher()
setupBackgroundLocationLauncher()
}
/**
@@ -43,6 +46,14 @@ class OnboardingCoordinator(
}
}
private fun setupBackgroundLocationLauncher() {
backgroundLocationLauncher = activity.registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
handleBackgroundLocationResult(granted)
}
}
/**
* Start the onboarding process
*/
@@ -50,9 +61,14 @@ class OnboardingCoordinator(
Log.d(TAG, "Starting onboarding process")
permissionManager.logPermissionStatus()
if (permissionManager.areAllPermissionsGranted()) {
Log.d(TAG, "All permissions already granted, completing onboarding")
if (permissionManager.areRequiredPermissionsGranted()) {
if (shouldRequestBackgroundLocation()) {
Log.d(TAG, "Foreground permissions granted; background location recommended")
onBackgroundLocationRequired()
} else {
Log.d(TAG, "Required permissions already granted, completing onboarding")
completeOnboarding()
}
} else {
Log.d(TAG, "Missing permissions, need to start explanation flow")
// The explanation screen will be shown by the calling activity
@@ -76,7 +92,11 @@ class OnboardingCoordinator(
val missingPermissions = (missingRequired + optionalToRequest).distinct()
if (missingPermissions.isEmpty()) {
if (shouldRequestBackgroundLocation()) {
onBackgroundLocationRequired()
} else {
completeOnboarding()
}
return
}
@@ -98,14 +118,20 @@ class OnboardingCoordinator(
val criticalGranted = criticalPermissions.all { permissions[it] == true }
when {
allGranted -> {
criticalGranted -> {
if (shouldRequestBackgroundLocation()) {
Log.d(TAG, "Foreground permissions granted; requesting background location next")
onBackgroundLocationRequired()
return
}
if (allGranted) {
Log.d(TAG, "All permissions granted successfully")
completeOnboarding()
}
criticalGranted -> {
} else {
Log.d(TAG, "Critical permissions granted, can proceed with limited functionality")
showPartialPermissionWarning(permissions)
}
}
else -> {
Log.d(TAG, "Critical permissions denied")
handlePermissionDenial(permissions)
@@ -113,15 +139,50 @@ class OnboardingCoordinator(
}
}
fun requestBackgroundLocation() {
val permission = permissionManager.getBackgroundLocationPermission()
if (permission == null) {
completeOnboarding()
return
}
Log.d(TAG, "Requesting background location permission")
backgroundLocationLauncher?.launch(permission)
}
private fun handleBackgroundLocationResult(granted: Boolean) {
if (granted) {
Log.d(TAG, "Background location permission granted")
} else {
Log.w(TAG, "Background location permission denied; continuing without it")
}
completeOnboarding()
}
fun skipBackgroundLocation() {
Log.d(TAG, "User skipped background location permission")
BackgroundLocationPreferenceManager.setSkipped(activity, true)
completeOnboarding()
}
fun checkBackgroundLocationAndProceed() {
if (!shouldRequestBackgroundLocation()) {
completeOnboarding()
}
}
private fun shouldRequestBackgroundLocation(): Boolean {
return permissionManager.needsBackgroundLocationPermission() &&
!permissionManager.isBackgroundLocationGranted() &&
!BackgroundLocationPreferenceManager.isSkipped(activity)
}
/**
* Get the list of critical permissions that are absolutely required
*/
private fun getCriticalPermissions(): List<String> {
// For bitchat, Bluetooth and location permissions are critical
// Notifications are nice-to-have but not critical
return permissionManager.getRequiredPermissions().filter { permission ->
!permission.contains("POST_NOTIFICATIONS")
}
// Notifications are nice-to-have but not critical and are not included in getRequiredPermissions()
return permissionManager.getRequiredPermissions()
}
/**
@@ -208,6 +269,7 @@ class OnboardingCoordinator(
private fun getPermissionDisplayName(permission: String): String {
return when {
permission.contains("BLUETOOTH") -> "Bluetooth/Nearby Devices"
permission.contains("BACKGROUND") -> "Background Location"
permission.contains("LOCATION") -> "Location (for Bluetooth scanning)"
permission.contains("NOTIFICATION") -> "Notifications"
else -> permission.substringAfterLast(".")
@@ -6,6 +6,7 @@ enum class OnboardingState {
LOCATION_CHECK,
BATTERY_OPTIMIZATION_CHECK,
PERMISSION_EXPLANATION,
BACKGROUND_LOCATION_EXPLANATION,
PERMISSION_REQUESTING,
INITIALIZING,
COMPLETE,
@@ -12,12 +12,10 @@ import androidx.compose.material.icons.filled.Power
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
@@ -209,29 +207,6 @@ private fun PermissionCategoryCard(
color = colorScheme.onBackground.copy(alpha = 0.8f)
)
if (category.type == PermissionType.PRECISE_LOCATION) {
// Extra emphasis for location permission
Spacer(modifier = Modifier.height(4.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = stringResource(R.string.cd_warning),
tint = Color(0xFFFF9800),
modifier = Modifier.size(16.dp)
)
Text(
text = stringResource(R.string.location_tracking_warning),
style = MaterialTheme.typography.bodySmall.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium,
color = Color(0xFFFF9800)
)
)
}
}
}
}
}
@@ -240,6 +215,7 @@ private fun getPermissionIcon(permissionType: PermissionType): ImageVector {
return when (permissionType) {
PermissionType.NEARBY_DEVICES -> Icons.Filled.Bluetooth
PermissionType.PRECISE_LOCATION -> Icons.Filled.LocationOn
PermissionType.BACKGROUND_LOCATION -> Icons.Filled.LocationOn
PermissionType.MICROPHONE -> Icons.Filled.Mic
PermissionType.NOTIFICATIONS -> Icons.Filled.Notifications
PermissionType.BATTERY_OPTIMIZATION -> Icons.Filled.Power
@@ -7,6 +7,7 @@ import android.os.Build
import android.os.PowerManager
import android.util.Log
import androidx.core.content.ContextCompat
import com.bitchat.android.R
/**
* Centralized permission management for bitchat app
@@ -40,7 +41,8 @@ class PermissionManager(private val context: Context) {
}
/**
* Get all permissions required by the app
* Get required permissions that can be requested together.
* Background location is handled separately to ensure correct request order.
* Note: Notification permission is optional and not included here,
* so the app works without notification access.
*/
@@ -72,6 +74,27 @@ class PermissionManager(private val context: Context) {
return permissions
}
/**
* Background location permission is required on Android 10+ for background BLE scanning.
* Must be requested after foreground location permissions are granted.
*/
fun needsBackgroundLocationPermission(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
fun getBackgroundLocationPermission(): String? {
return if (needsBackgroundLocationPermission()) {
Manifest.permission.ACCESS_BACKGROUND_LOCATION
} else {
null
}
}
fun isBackgroundLocationGranted(): Boolean {
val permission = getBackgroundLocationPermission() ?: return true
return isPermissionGranted(permission)
}
/**
* Get optional permissions that improve the experience but aren't required.
* Currently includes POST_NOTIFICATIONS on Android 13+.
@@ -93,9 +116,13 @@ class PermissionManager(private val context: Context) {
}
/**
* Check if all required permissions are granted
* Check if all required permissions are granted (background location is optional).
*/
fun areAllPermissionsGranted(): Boolean {
return areRequiredPermissionsGranted()
}
fun areRequiredPermissionsGranted(): Boolean {
return getRequiredPermissions().all { isPermissionGranted(it) }
}
@@ -131,6 +158,11 @@ class PermissionManager(private val context: Context) {
return getRequiredPermissions().filter { !isPermissionGranted(it) }
}
fun getMissingBackgroundLocationPermission(): List<String> {
val permission = getBackgroundLocationPermission() ?: return emptyList()
return if (isPermissionGranted(permission)) emptyList() else listOf(permission)
}
/**
* Get categorized permission information for display
*/
@@ -177,6 +209,19 @@ class PermissionManager(private val context: Context) {
)
)
if (needsBackgroundLocationPermission()) {
val backgroundPermission = listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
categories.add(
PermissionCategory(
type = PermissionType.BACKGROUND_LOCATION,
description = context.getString(R.string.perm_background_location_desc),
permissions = backgroundPermission,
isGranted = backgroundPermission.all { isPermissionGranted(it) },
systemDescription = context.getString(R.string.perm_background_location_system)
)
)
}
// Notifications category (if applicable)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
categories.add(
@@ -216,7 +261,7 @@ class PermissionManager(private val context: Context) {
appendLine("Permission Diagnostics:")
appendLine("Android SDK: ${Build.VERSION.SDK_INT}")
appendLine("First time launch: ${isFirstTimeLaunch()}")
appendLine("All permissions granted: ${areAllPermissionsGranted()}")
appendLine("Required permissions granted: ${areAllPermissionsGranted()}")
appendLine()
getCategorizedPermissions().forEach { category ->
@@ -228,7 +273,7 @@ class PermissionManager(private val context: Context) {
appendLine()
}
val missing = getMissingPermissions()
val missing = getMissingPermissions() + getMissingBackgroundLocationPermission()
if (missing.isNotEmpty()) {
appendLine("Missing permissions:")
missing.forEach { permission ->
@@ -260,6 +305,7 @@ data class PermissionCategory(
enum class PermissionType(val nameValue: String) {
NEARBY_DEVICES("Nearby Devices"),
PRECISE_LOCATION("Precise Location"),
BACKGROUND_LOCATION("Background Location"),
MICROPHONE("Microphone"),
NOTIFICATIONS("Notifications"),
BATTERY_OPTIMIZATION("Battery Optimization"),
@@ -7,6 +7,7 @@ import android.app.PendingIntent
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 androidx.core.app.NotificationCompat
@@ -176,7 +177,7 @@ class MeshForegroundService : Service() {
// If we became eligible and are not in foreground yet, promote once
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions() && !isInForeground) {
val n = buildNotification(meshService?.getActivePeerCount() ?: 0)
startForeground(NOTIFICATION_ID, n)
startForegroundCompat(n)
isInForeground = true
} else {
updateNotification(force = true)
@@ -191,7 +192,7 @@ class MeshForegroundService : Service() {
// Promote exactly once when eligible, otherwise stay background (or stop)
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions() && !isInForeground) {
val notification = buildNotification(meshService?.getActivePeerCount() ?: 0)
startForeground(NOTIFICATION_ID, notification)
startForegroundCompat(notification)
isInForeground = true
}
@@ -326,6 +327,35 @@ class MeshForegroundService : Service() {
}
}
private fun hasLocationPermission(): Boolean {
val fine = androidx.core.content.ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) == android.content.pm.PackageManager.PERMISSION_GRANTED
val coarse = androidx.core.content.ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) == android.content.pm.PackageManager.PERMISSION_GRANTED
return fine || coarse
}
private fun startForegroundCompat(notification: Notification) {
if (Build.VERSION.SDK_INT >= 34) {
val type = if (hasLocationPermission()) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE or ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
} else {
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
}
try {
startForeground(NOTIFICATION_ID, notification, type)
} catch (e: SecurityException) {
// Fallback for cases where "While In Use" permission exists but background start is restricted
if (type and ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION != 0) {
android.util.Log.w("MeshForegroundService", "Failed to start with LOCATION type, falling back to CONNECTED_DEVICE: ${e.message}")
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE)
} else {
throw e
}
}
} else {
startForeground(NOTIFICATION_ID, notification)
}
}
override fun onDestroy() {
updateJob?.cancel()
updateJob = null
+12
View File
@@ -320,6 +320,15 @@
<string name="location_explanation">bitchat does NOT track your location.\n\nLocation services are required for Bluetooth scanning and for the Geohash chat feature.</string>
<string name="location_needs_for">bitchat needs location services for:</string>
<string name="location_needs_bullets">• Bluetooth device scanning\n• Discovering nearby users on mesh network\n• Geohash chat feature\n• No tracking or location collection</string>
<string name="background_location_required_title">Background Location Recommended</string>
<string name="background_location_required_subtitle">optional, improves mesh reliability</string>
<string name="background_location_explanation">Android recommends background location so bitchat can scan for nearby devices while the app is not open. This keeps the mesh alive after reboot.</string>
<string name="background_location_settings_tip">When settings opens, choose "Allow all the time".</string>
<string name="background_location_needs_for">bitchat uses background location for:</string>
<string name="background_location_needs_bullets">- scan for nearby devices while the app is closed\n- reconnect after reboot\n- keep the mesh running in the background</string>
<string name="background_location_privacy_note">We NEVER collect or store your location. Your privacy is safe.</string>
<string name="grant_background_location">Allow Background Location</string>
<string name="skip_background_location">Continue without background location</string>
<string name="open_location_settings">Open Location Settings</string>
<string name="check_again">Check Again</string>
<string name="location_services_unavailable">Location Services Unavailable</string>
@@ -405,6 +414,8 @@
<string name="perm_nearby_devices_system">Allow bitchat to connect to nearby devices</string>
<string name="perm_location_desc">Required by Android to discover nearby bitchat users via Bluetooth</string>
<string name="perm_location_system">bitchat needs this to scan for nearby devices</string>
<string name="perm_background_location_desc">Recommended to scan for nearby devices while the app is in the background</string>
<string name="perm_background_location_system">Allow background location access</string>
<string name="perm_notifications_desc">Receive notifications when you receive private messages</string>
<string name="perm_notifications_system">Allow bitchat to send you notifications</string>
<string name="perm_battery_desc">Disable battery optimization to ensure bitchat runs reliably in the background and maintains mesh network connections</string>
@@ -413,6 +424,7 @@
<!-- Permission types -->
<string name="perm_type_nearby_devices">Nearby Devices</string>
<string name="perm_type_precise_location">Precise Location</string>
<string name="perm_type_background_location">Background Location</string>
<string name="perm_type_microphone">Microphone</string>
<string name="perm_type_notifications">Notifications</string>
<string name="perm_type_battery_optimization">Battery Optimization</string>