diff --git a/app/src/main/java/org/obd/graphs/activity/MainActivity.kt b/app/src/main/java/org/obd/graphs/activity/MainActivity.kt index b7327e8e..34aa6a3d 100644 --- a/app/src/main/java/org/obd/graphs/activity/MainActivity.kt +++ b/app/src/main/java/org/obd/graphs/activity/MainActivity.kt @@ -20,15 +20,10 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.res.Configuration -import android.net.Uri -import android.os.Build import android.os.Bundle -import android.os.PowerManager import android.os.StrictMode import android.os.StrictMode.ThreadPolicy import android.os.StrictMode.VmPolicy -import android.provider.Settings -import android.util.Log import android.view.View import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity @@ -41,6 +36,7 @@ import org.obd.graphs.BuildConfig import org.obd.graphs.ExceptionHandler import org.obd.graphs.MAIN_ACTIVITY_EVENT_DESTROYED import org.obd.graphs.MAIN_ACTIVITY_EVENT_PAUSE +import org.obd.graphs.Permissions import org.obd.graphs.R import org.obd.graphs.bl.datalogger.dataLogger import org.obd.graphs.bl.drag.dragRacingMetricsProcessor @@ -161,13 +157,14 @@ class MainActivity : setupLeftNavigationPanel() supportActionBar?.hide() setupMetricsProcessors() - setupBatteryOptimization() backupManager = BackupManager(this) displayAppSignature(this) navigateToLastVisitedScreen() + validatePermissions() } + override fun onResume() { super.onResume() screen.setupWindowManager(this) @@ -258,22 +255,9 @@ class MainActivity : } } - private fun setupBatteryOptimization() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val registered = (getSystemService(POWER_SERVICE) as PowerManager).isIgnoringBatteryOptimizations(packageName) - Log.i( - LOG_TAG, - "Checking permissions related to battery optimization. Ignoring Battery Optimization for package=$packageName, " + - "registered=$registered", - ) - if (!registered) { - startActivity( - Intent().apply { - action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS - data = Uri.parse("package:$packageName") - }, - ) - } + private fun validatePermissions() { + if (Permissions.isAnyPermissionMissing(this)) { + Permissions.showPermissionOnboarding(this) } } } diff --git a/common/src/main/java/org/obd/graphs/Permissions.kt b/common/src/main/java/org/obd/graphs/Permissions.kt index 14489598..81ced753 100644 --- a/common/src/main/java/org/obd/graphs/Permissions.kt +++ b/common/src/main/java/org/obd/graphs/Permissions.kt @@ -17,11 +17,16 @@ package org.obd.graphs import android.Manifest +import android.annotation.SuppressLint import android.app.Activity import android.content.Context +import android.content.Intent import android.content.pm.PackageManager import android.location.LocationManager +import android.net.Uri import android.os.Build +import android.os.PowerManager +import android.provider.Settings import android.util.Log import androidx.core.content.ContextCompat import androidx.core.location.LocationManagerCompat @@ -33,7 +38,60 @@ private const val LOCATION_REQUEST_CODE = 1001 private const val BLUETOOTH_REQUEST_CODE = 1002 private const val NOTIFICATION_REQUEST_CODE = 1003 -object Permissions { + @SuppressLint("ObsoleteSdkInt") + object Permissions { + + /** + * Returns TRUE if any required permission is missing. + * Reuses your existing individual checks. + */ + fun isAnyPermissionMissing(context: Context): Boolean { + if (!hasLocationPermissions(context)) return true + if (!hasNotificationPermissions(context)) return true + if (!isBatteryOptimizationEnabled(context)) return true + + val btPerms = getBluetoothPermissions() + return !EasyPermissions.hasPermissions(context, *btPerms) + } + + fun showPermissionOnboarding(activity: Activity) { + val message = + """ + To provide full functionality, please allow the following in the next steps: + • Battery Optimization: To ensure data logging isn't interrupted in the background. + • Location: To track your trip via GPS. + • Bluetooth: To connect to your OBD adapter. + • Notifications: To keep the logger running in the background. + """.trimIndent() + + androidx.appcompat.app.AlertDialog + .Builder(activity) + .setTitle("Setup Required") + .setMessage(message) + .setPositiveButton("Begin Setup") { _, _ -> + requestAll(activity) + + if (!isBatteryOptimizationEnabled(activity)) { + requestBatteryOptimization(activity) + } + }.setNegativeButton("Later", null) + .show() + } + + + + /** + * Checks if the app is already ignoring battery optimizations. + */ + @SuppressLint("ObsoleteSdkInt") + fun isBatteryOptimizationEnabled(context: Context): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as? PowerManager + return powerManager?.isIgnoringBatteryOptimizations(context.packageName) ?: true + } + return true + } + /** * Returns TRUE if all required location permissions are granted. * Also performs a diagnostic check to warn if the user has selected "Approximate" location. @@ -165,4 +223,57 @@ object Permissions { } return perms.toTypedArray() } + + /** + * Aggregates and requests all required permissions for the application. + * Use this at app startup to minimize the number of pop-ups. + */ + private fun requestAll(activity: Activity) { + val perms = mutableListOf() + + perms.add(Manifest.permission.ACCESS_COARSE_LOCATION) + perms.add(Manifest.permission.ACCESS_FINE_LOCATION) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + perms.add(Manifest.permission.BLUETOOTH_SCAN) + perms.add(Manifest.permission.BLUETOOTH_CONNECT) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + perms.add(Manifest.permission.POST_NOTIFICATIONS) + } + + val missingPermissions = + perms.filter { + ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED + } + + if (missingPermissions.isNotEmpty()) { + Log.i(TAG, "Requesting missing permissions: $missingPermissions") + + EasyPermissions.requestPermissions( + activity, + "This app requires Location, Bluetooth, and Notification permissions to function correctly.", + 1000, // Use a generic ALL_PERMISSIONS_REQUEST_CODE + *missingPermissions.toTypedArray(), + ) + } else { + Log.v(TAG, "All permissions already granted.") + } + } + + /** + * Triggers the battery optimization intent. + * Note: This must be called from an Activity. + */ + private fun requestBatteryOptimization(activity: Activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Log.i(TAG, "Requesting to ignore battery optimizations.") + val intent = Intent().apply { + action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + data = Uri.parse("package:${activity.packageName}") + } + activity.startActivity(intent) + } + } } diff --git a/datalogger/src/main/java/org/obd/graphs/bl/gps/GpsMetricsEmitter.kt b/datalogger/src/main/java/org/obd/graphs/bl/gps/GpsMetricsEmitter.kt index 3bd284d8..74ff10fd 100644 --- a/datalogger/src/main/java/org/obd/graphs/bl/gps/GpsMetricsEmitter.kt +++ b/datalogger/src/main/java/org/obd/graphs/bl/gps/GpsMetricsEmitter.kt @@ -94,7 +94,6 @@ internal class GpsMetricsEmitter : MetricsProcessor { } override fun onStopped() { - Log.i(TAG, "Stopping GPS updates") stopGpsUpdates() } @@ -147,6 +146,9 @@ internal class GpsMetricsEmitter : MetricsProcessor { private fun stopGpsUpdates() { try { + + Log.i(TAG, "Stopping GPS updates") + locationListener?.let { locationManager?.removeUpdates(it) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { diff --git a/profile/src/main/java/org/obd/graphs/profile/DefaultProfileService.kt b/profile/src/main/java/org/obd/graphs/profile/DefaultProfileService.kt index ac475688..a82af806 100644 --- a/profile/src/main/java/org/obd/graphs/profile/DefaultProfileService.kt +++ b/profile/src/main/java/org/obd/graphs/profile/DefaultProfileService.kt @@ -164,7 +164,11 @@ internal class DefaultProfileService : } if (pref.startsWith("profile_") || pref == getInstallationVersion()) { - Log.v(PROFILE_AUTO_SAVER_LOG_TAG, "Skipping: $pref") + if (Log.isLoggable(PROFILE_AUTO_SAVER_LOG_TAG,Log.VERBOSE)) { + Log.v(PROFILE_AUTO_SAVER_LOG_TAG, "Skipping: $pref") + } else { + // + } } else { val profileName = getCurrentProfile() ss?.edit {