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