diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d88de417..5ae8b7f9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -23,6 +23,8 @@
android:maxSdkVersion="30" />
+
+
diff --git a/app/src/main/java/eu/darken/capod/common/permissions/Permission.kt b/app/src/main/java/eu/darken/capod/common/permissions/Permission.kt
index 251a3b29..8756dd37 100644
--- a/app/src/main/java/eu/darken/capod/common/permissions/Permission.kt
+++ b/app/src/main/java/eu/darken/capod/common/permissions/Permission.kt
@@ -3,9 +3,11 @@ package eu.darken.capod.common.permissions
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
+import android.os.PowerManager
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import eu.darken.capod.R
+import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.withinApiLevel
enum class Permission(
@@ -14,6 +16,9 @@ enum class Permission(
@StringRes val labelRes: Int,
@StringRes val descriptionRes: Int,
val permissionId: String,
+ val isGranted: (Context) -> Boolean = {
+ ContextCompat.checkSelfPermission(it, permissionId) == PackageManager.PERMISSION_GRANTED
+ },
) {
BLUETOOTH(
minApiLevel = Build.VERSION_CODES.BASE,
@@ -47,10 +52,20 @@ enum class Permission(
labelRes = R.string.permission_background_location_label,
descriptionRes = R.string.permission_background_location_description,
permissionId = "android.permission.ACCESS_BACKGROUND_LOCATION",
+ ),
+ IGNORE_BATTERY_OPTIMIZATION(
+ minApiLevel = Build.VERSION_CODES.BASE,
+ labelRes = R.string.permission_ignore_battery_optimizations_label,
+ descriptionRes = R.string.permission_ignore_battery_optimizations_description,
+ permissionId = "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS",
+ isGranted = {
+ val pwm = it.getSystemService(Context.POWER_SERVICE) as PowerManager
+ pwm.isIgnoringBatteryOptimizations(BuildConfigWrap.APPLICATION_ID)
+ },
)
}
fun Permission.isRequired(context: Context): Boolean = when {
!withinApiLevel(minApiLevel, maxApiLevel) -> false
- else -> ContextCompat.checkSelfPermission(context, permissionId) != PackageManager.PERMISSION_GRANTED
+ else -> !isGranted(context)
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/main/core/PermissionTool.kt b/app/src/main/java/eu/darken/capod/main/core/PermissionTool.kt
new file mode 100644
index 00000000..503f7a86
--- /dev/null
+++ b/app/src/main/java/eu/darken/capod/main/core/PermissionTool.kt
@@ -0,0 +1,20 @@
+package eu.darken.capod.main.core
+
+import android.content.Context
+import dagger.Reusable
+import dagger.hilt.android.qualifiers.ApplicationContext
+import eu.darken.capod.common.permissions.Permission
+import eu.darken.capod.common.permissions.isRequired
+import javax.inject.Inject
+
+@Reusable
+class PermissionTool @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val generalSettings: GeneralSettings,
+) {
+
+ suspend fun missingPermissions(): Set = Permission.values()
+ .filter { it != Permission.IGNORE_BATTERY_OPTIMIZATION || generalSettings.monitorMode.value == MonitorMode.ALWAYS }
+ .filter { it.isRequired(context) }
+ .toSet()
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragment.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragment.kt
index 299782c1..629f7db7 100644
--- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragment.kt
+++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragment.kt
@@ -1,6 +1,9 @@
package eu.darken.capod.main.ui.overview
+import android.content.Intent
+import android.net.Uri
import android.os.Bundle
+import android.provider.Settings
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
@@ -10,11 +13,13 @@ import eu.darken.capod.R
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.lists.differ.update
import eu.darken.capod.common.lists.setupDefaults
+import eu.darken.capod.common.permissions.Permission
import eu.darken.capod.common.uix.Fragment3
import eu.darken.capod.common.viewbinding.viewBinding
import eu.darken.capod.databinding.MainFragmentBinding
import javax.inject.Inject
+
@AndroidEntryPoint
class OverviewFragment : Fragment3(R.layout.main_fragment) {
@@ -25,9 +30,11 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
lateinit var adapter: OverviewAdapter
lateinit var permissionLauncher: ActivityResultLauncher
+ var awaitingIgnoreBatteryOptimization = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ awaitingIgnoreBatteryOptimization = savedInstanceState?.getBoolean("awaitingIgnoreBatteryOptimization") ?: false
permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
log { "Request for $id was granted=$granted" }
@@ -56,9 +63,33 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
adapter.update(it)
}
- vm.requestPermissionevent.observe2(ui) {
- permissionLauncher.launch(it.permissionId)
+ vm.requestPermissionEvent.observe2(ui) {
+ if (it == Permission.IGNORE_BATTERY_OPTIMIZATION) {
+ awaitingIgnoreBatteryOptimization = true
+ startActivity(
+ Intent(
+ Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
+ Uri.parse("package:${requireContext().packageName}")
+ )
+ )
+ } else {
+ permissionLauncher.launch(it.permissionId)
+ }
}
super.onViewCreated(view, savedInstanceState)
}
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ outState.putBoolean("awaitingIgnoreBatteryOptimization", awaitingIgnoreBatteryOptimization)
+ super.onSaveInstanceState(outState)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (awaitingIgnoreBatteryOptimization) {
+ awaitingIgnoreBatteryOptimization = false
+ log { "awaitingIgnoreBatteryOptimization=true" }
+ vm.onPermissionResult(true)
+ }
+ }
}
diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt
index 9e99e546..d9461f42 100644
--- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt
+++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt
@@ -1,21 +1,18 @@
package eu.darken.capod.main.ui.overview
-import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
-import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.autoreport.DebugSettings
import eu.darken.capod.common.debug.logging.log
-import eu.darken.capod.common.debug.recording.core.RecorderModule
import eu.darken.capod.common.livedata.SingleLiveEvent
import eu.darken.capod.common.navigation.navVia
import eu.darken.capod.common.permissions.Permission
-import eu.darken.capod.common.permissions.isRequired
import eu.darken.capod.common.uix.ViewModel3
import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.main.core.MonitorMode
+import eu.darken.capod.main.core.PermissionTool
import eu.darken.capod.main.ui.overview.cards.PermissionCardVH
import eu.darken.capod.main.ui.overview.cards.pods.BasicSingleApplePodsCardVH
import eu.darken.capod.main.ui.overview.cards.pods.DualApplePodsCardVH
@@ -38,13 +35,12 @@ import javax.inject.Inject
@HiltViewModel
class OverviewFragmentVM @Inject constructor(
handle: SavedStateHandle,
- @ApplicationContext private val context: Context,
dispatcherProvider: DispatcherProvider,
- private val recorderModule: RecorderModule,
private val monitorControl: MonitorControl,
private val podMonitor: PodMonitor,
+ private val permissionTool: PermissionTool,
private val generalSettings: GeneralSettings,
- private val debugSettings: DebugSettings,
+ debugSettings: DebugSettings,
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
private val updateTicker = channelFlow {
@@ -55,10 +51,8 @@ class OverviewFragmentVM @Inject constructor(
}
private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID())
- private val requiredPermissions: Flow> = permissionCheckTrigger
- .map {
- Permission.values().filter { it.isRequired(context) }
- }
+ private val requiredPermissions: Flow> = permissionCheckTrigger
+ .map { permissionTool.missingPermissions() }
.onEach {
log(TAG) { "Missing permissions: $it" }
if (it.isEmpty() && generalSettings.monitorMode.value != MonitorMode.MANUAL) {
@@ -67,7 +61,7 @@ class OverviewFragmentVM @Inject constructor(
}
}
- val requestPermissionevent = SingleLiveEvent()
+ val requestPermissionEvent = SingleLiveEvent()
private val pods: Flow> = requiredPermissions
.flatMapLatest { permissions ->
@@ -99,15 +93,6 @@ class OverviewFragmentVM @Inject constructor(
) { tick, permissions, pods, isDebugMode ->
val items = mutableListOf()
- permissions
- .map {
- PermissionCardVH.Item(
- permission = it,
- onRequest = { requestPermissionevent.postValue(it) }
- )
- }
- .forEach { items.add(it) }
-
val now = Instant.now()
pods
.map {
@@ -135,6 +120,15 @@ class OverviewFragmentVM @Inject constructor(
}
.run { items.addAll(this) }
+ permissions
+ .map {
+ PermissionCardVH.Item(
+ permission = it,
+ onRequest = { requestPermissionEvent.postValue(it) }
+ )
+ }
+ .run { items.addAll(this) }
+
items
}
.catch { errorEvents.postValue(it) }
@@ -144,10 +138,6 @@ class OverviewFragmentVM @Inject constructor(
if (granted) permissionCheckTrigger.value = UUID.randomUUID()
}
- fun toggleDebugLog() = launch {
- recorderModule.startRecorder()
- }
-
fun goToSettings() = launch {
OverviewFragmentDirections.actionOverviewFragmentToSettingsFragment().navVia(this@OverviewFragmentVM)
}
diff --git a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt
index ee833c66..92be633c 100644
--- a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt
+++ b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt
@@ -16,10 +16,9 @@ import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.common.flow.withPrevious
-import eu.darken.capod.common.permissions.Permission
-import eu.darken.capod.common.permissions.isRequired
import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.main.core.MonitorMode
+import eu.darken.capod.main.core.PermissionTool
import eu.darken.capod.monitor.core.MonitorComponent
import eu.darken.capod.monitor.core.MonitorCoroutineScope
import eu.darken.capod.monitor.ui.MonitorNotifications
@@ -41,6 +40,7 @@ class MonitorWorker @AssistedInject constructor(
private val notificationManager: NotificationManager,
private val generalSettings: GeneralSettings,
private val mediaControl: MediaControl,
+ private val permissionTool: PermissionTool,
) : CoroutineWorker(context, params) {
private val workerScope = MonitorCoroutineScope()
@@ -91,14 +91,21 @@ class MonitorWorker @AssistedInject constructor(
}
private suspend fun doDoWork() {
- val missingPermissions = Permission.values().filter { it.isRequired(context) }
- if (missingPermissions.isNotEmpty()) {
- log(TAG, WARN) { "Aborting, missing permissions: $missingPermissions" }
+ val permissionsMissingOnStart = permissionTool.missingPermissions()
+ if (permissionsMissingOnStart.isNotEmpty()) {
+ log(TAG, WARN) { "Aborting, missing permissions: $permissionsMissingOnStart" }
return
}
generalSettings.monitorMode.flow
.flatMapLatest { monitorMode ->
+ val missingPermsFlow = permissionTool.missingPermissions()
+ if (missingPermsFlow.isNotEmpty()) {
+ log(TAG, WARN) { "Aborting, permissions are missing for $monitorMode: $missingPermsFlow" }
+ workerScope.coroutineContext.cancelChildren()
+ return@flatMapLatest emptyFlow()
+ }
+
bluetoothManager2
.isBluetoothEnabled
.flatMapLatest { bluetoothManager2.connectedDevices() }
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e1fc60ec..41a83865 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -24,8 +24,10 @@
Required to be able to discover and pair nearby Bluetooth devices.
BLUETOOTH
Allows applications to connect to paired bluetooth devices.
- Allows an app to access precise location.
ACCESS_FINE_LOCATION
+ Allows an app to access precise location.
+ ACCESS_BACKGROUND_LOCATION
+ Allows an app to access location in the background.
Left pod
Right pod
@@ -101,10 +103,10 @@
Start music playback music when wearing the device (if supported).
L %1$s | C %2$s | R %3$s
Headphones: %1$s
- ACCESS_BACKGROUND_LOCATION
- Allows an app to access location in the background.
Fake data
Show fake data, i.e. simulate device that don\'t exist.
Debug settings
Additional settings to help troubleshoot issues with the app.
+ IGNORE_BATTERY_OPTIMIZATIONS
+ IGNORE_BATTERY_OPTIMIZATIONS
\ No newline at end of file