mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-10-07 03:42:59 +00:00
Use broadcastReceiverFlow instead of BroadcastReceivers (#722)
* [WIP] Use broadcastReceiverFlow and packageReceiverFlow to make sure that broadcast receivers are unregistered * Rewrite remaining BroadcastReceivers to use broadcastReceiverFlow * TasksAppWatcher: minor coroutine changes * KDoc * TasksActivity: use Compose state instead of LiveData
This commit is contained in:
parent
463c18c4fb
commit
c0570549c9
|
@ -14,6 +14,8 @@ import at.bitfire.davdroid.ui.DebugInfoActivity
|
|||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.davdroid.ui.UiUtils
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import java.util.logging.Level
|
||||
import javax.inject.Inject
|
||||
import kotlin.concurrent.thread
|
||||
|
@ -23,8 +25,6 @@ import kotlin.system.exitProcess
|
|||
class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provider {
|
||||
|
||||
@Inject lateinit var accountsUpdatedListener: AccountsUpdatedListener
|
||||
@Inject lateinit var storageLowReceiver: StorageLowReceiver
|
||||
|
||||
@Inject lateinit var workerFactory: HiltWorkerFactory
|
||||
|
||||
override val workManagerConfiguration: Configuration
|
||||
|
@ -57,15 +57,13 @@ class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provide
|
|||
// some current activity and causes an IllegalStateException in rare cases
|
||||
|
||||
// don't block UI for some background checks
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
thread {
|
||||
// watch for account changes/deletions
|
||||
accountsUpdatedListener.listen()
|
||||
|
||||
// watch storage because low storage means sync framework stops local content update notifications
|
||||
storageLowReceiver.listen()
|
||||
|
||||
// watch installed/removed tasks apps and update sync settings accordingly
|
||||
TasksWatcher.watch(this)
|
||||
// watch installed/removed tasks apps over whole app lifetime and update sync settings accordingly
|
||||
TasksAppWatcher.watchInstalledTaskApps(this, GlobalScope)
|
||||
|
||||
// create/update app shortcuts
|
||||
UiUtils.updateShortcuts(this)
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.annotation.MainThread
|
||||
|
||||
abstract class PackageChangedReceiver(
|
||||
val context: Context
|
||||
): BroadcastReceiver(), AutoCloseable {
|
||||
|
||||
/**
|
||||
* Registers the receiver.
|
||||
*
|
||||
* @param whether [onPackageChanged] shall be called immediately after registering
|
||||
*/
|
||||
fun register(immediateCall: Boolean = false) {
|
||||
val filter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
|
||||
addAction(Intent.ACTION_PACKAGE_CHANGED)
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addDataScheme("package")
|
||||
}
|
||||
context.registerReceiver(this, filter)
|
||||
|
||||
if (immediateCall)
|
||||
onPackageChanged()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
context.unregisterReceiver(this)
|
||||
}
|
||||
|
||||
|
||||
@MainThread
|
||||
abstract fun onPackageChanged()
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
onPackageChanged()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
class StorageLowReceiver private constructor(
|
||||
val context: Context
|
||||
): BroadcastReceiver(), AutoCloseable {
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object StorageLowReceiverModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun storageLowReceiver(@ApplicationContext context: Context) = StorageLowReceiver(context)
|
||||
}
|
||||
|
||||
|
||||
val storageLow = MutableLiveData(false)
|
||||
|
||||
fun listen() {
|
||||
Logger.log.fine("Listening for device storage low/OK broadcasts")
|
||||
val filter = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_DEVICE_STORAGE_LOW)
|
||||
addAction(Intent.ACTION_DEVICE_STORAGE_OK)
|
||||
}
|
||||
context.registerReceiver(this, filter)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
context.unregisterReceiver(this)
|
||||
}
|
||||
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
Intent.ACTION_DEVICE_STORAGE_LOW -> onStorageLow()
|
||||
Intent.ACTION_DEVICE_STORAGE_OK -> onStorageOk()
|
||||
}
|
||||
}
|
||||
|
||||
fun onStorageLow() {
|
||||
Logger.log.warning("Low storage, sync will not be started by Android!")
|
||||
|
||||
storageLow.postValue(true)
|
||||
}
|
||||
|
||||
fun onStorageOk() {
|
||||
Logger.log.info("Storage OK again")
|
||||
|
||||
storageLow.postValue(false)
|
||||
}
|
||||
|
||||
}
|
53
app/src/main/kotlin/at/bitfire/davdroid/TasksAppWatcher.kt
Normal file
53
app/src/main/kotlin/at/bitfire/davdroid/TasksAppWatcher.kt
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.content.Context
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.util.TaskUtils
|
||||
import at.bitfire.davdroid.util.packageChangedFlow
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Watches whether a tasks app has been installed or uninstalled and updates
|
||||
* the selected tasks app and task sync settings accordingly.
|
||||
*/
|
||||
object TasksAppWatcher {
|
||||
|
||||
fun watchInstalledTaskApps(context: Context, externalScope: CoroutineScope) {
|
||||
externalScope.launch(Dispatchers.Default) {
|
||||
packageChangedFlow(context).collect {
|
||||
onPackageChanged(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPackageChanged(context: Context) {
|
||||
val currentProvider = TaskUtils.currentProvider(context)
|
||||
Logger.log.info("App launched or package (un)installed; current tasks provider = $currentProvider")
|
||||
|
||||
if (currentProvider == null) {
|
||||
// Iterate through all supported providers and select one, if available.
|
||||
var providerSelected = false
|
||||
for (provider in TaskProvider.ProviderName.entries) {
|
||||
val available = context.packageManager.resolveContentProvider(provider.authority, 0) != null
|
||||
if (available) {
|
||||
Logger.log.info("Selecting new tasks provider: $provider")
|
||||
TaskUtils.selectProvider(context, provider)
|
||||
providerSelected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!providerSelected)
|
||||
// no provider available (anymore), also clear setting and sync
|
||||
TaskUtils.selectProvider(context, null)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.content.Context
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.util.TaskUtils
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Watches whether a tasks app has been installed or uninstalled and updates
|
||||
* the selected tasks app and task sync settings accordingly.
|
||||
*/
|
||||
class TasksWatcher private constructor(
|
||||
context: Context
|
||||
): PackageChangedReceiver(context) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun watch(context: Context) {
|
||||
TasksWatcher(context).register(true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onPackageChanged() {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
val currentProvider = TaskUtils.currentProvider(context)
|
||||
Logger.log.info("App launched or package (un)installed; current tasks provider = $currentProvider")
|
||||
|
||||
if (currentProvider == null) {
|
||||
// Iterate through all supported providers and select one, if available.
|
||||
var providerSelected = false
|
||||
for (provider in TaskProvider.ProviderName.entries) {
|
||||
val available = context.packageManager.resolveContentProvider(provider.authority, 0) != null
|
||||
if (available) {
|
||||
Logger.log.info("Selecting new tasks provider: $provider")
|
||||
TaskUtils.selectProvider(context, provider)
|
||||
providerSelected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!providerSelected)
|
||||
// no provider available (anymore), also clear setting and sync
|
||||
TaskUtils.selectProvider(context, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -73,6 +73,7 @@ import androidx.core.content.getSystemService
|
|||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
|
@ -196,19 +197,19 @@ class AccountsActivity: AppCompatActivity() {
|
|||
if (intent.resolveActivity(packageManager) != null)
|
||||
startActivity(intent)
|
||||
},
|
||||
dataSaverActive = warnings.dataSaverEnabled.observeAsState().value == true,
|
||||
dataSaverActive = warnings.dataSaverEnabled.collectAsStateWithLifecycle().value,
|
||||
onManageDataSaver = {
|
||||
val intent = Intent(Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS, Uri.parse("package:$packageName"))
|
||||
if (intent.resolveActivity(packageManager) != null)
|
||||
startActivity(intent)
|
||||
},
|
||||
batterySaverActive = warnings.batterySaverActive.observeAsState().value == true,
|
||||
batterySaverActive = warnings.batterySaverActive.collectAsStateWithLifecycle().value,
|
||||
onManageBatterySaver = {
|
||||
val intent = Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS)
|
||||
if (intent.resolveActivity(packageManager) != null)
|
||||
startActivity(intent)
|
||||
},
|
||||
lowStorageWarning = warnings.storageLow.observeAsState().value == true,
|
||||
lowStorageWarning = warnings.storageLow.collectAsStateWithLifecycle().value,
|
||||
onManageStorage = {
|
||||
val intent = Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS)
|
||||
if (intent.resolveActivity(packageManager) != null)
|
||||
|
|
|
@ -6,8 +6,6 @@ package at.bitfire.davdroid.ui
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.SharedPreferences
|
||||
|
@ -59,6 +57,7 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.preference.PreferenceManager
|
||||
import at.bitfire.cert4android.CustomCertStore
|
||||
|
@ -76,9 +75,13 @@ import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage
|
|||
import at.bitfire.davdroid.ui.intro.OpenSourcePage
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.util.TaskUtils
|
||||
import at.bitfire.davdroid.util.broadcastReceiverFlow
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -136,7 +139,7 @@ class AppSettingsActivity: AppCompatActivity() {
|
|||
AppSettings_Debugging(
|
||||
verboseLogging = model.getPrefBoolean(Logger.LOG_TO_FILE).observeAsState().value ?: false,
|
||||
onUpdateVerboseLogging = { model.putPrefBoolean(Logger.LOG_TO_FILE, it) },
|
||||
batterySavingExempted = model.getBatterySavingExempted().observeAsState(false).value,
|
||||
batterySavingExempted = model.batterySavingExempted.collectAsStateWithLifecycle().value,
|
||||
onExemptFromBatterySaving = {
|
||||
startActivity(Intent(
|
||||
android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
||||
|
@ -494,29 +497,10 @@ class AppSettingsActivity: AppCompatActivity() {
|
|||
|
||||
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
fun getBatterySavingExempted(): LiveData<Boolean> = object : LiveData<Boolean>() {
|
||||
val receiver = object: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
update()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActive() {
|
||||
context.registerReceiver(receiver, IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED))
|
||||
update()
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
context.getSystemService<PowerManager>()?.let { powerManager ->
|
||||
val exempted = powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)
|
||||
postValue(exempted)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val powerManager = context.getSystemService<PowerManager>()!!
|
||||
val batterySavingExempted = broadcastReceiverFlow(context, IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED))
|
||||
.map { powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
fun getPrefBoolean(keyToObserve: String): LiveData<Boolean?> =
|
||||
object : LiveData<Boolean?>(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.app.Application
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.SyncStatusObserver
|
||||
|
@ -17,10 +15,14 @@ import android.net.NetworkCapabilities
|
|||
import android.net.NetworkRequest
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import at.bitfire.davdroid.StorageLowReceiver
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.bitfire.davdroid.util.broadcastReceiverFlow
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -37,12 +39,23 @@ import javax.inject.Inject
|
|||
*/
|
||||
@HiltViewModel
|
||||
class AppWarningsModel @Inject constructor(
|
||||
context: Application,
|
||||
storageLowReceiver: StorageLowReceiver
|
||||
): AndroidViewModel(context), SyncStatusObserver {
|
||||
val context: Application
|
||||
): ViewModel(), SyncStatusObserver {
|
||||
|
||||
/** whether storage is low (prevents sync framework from running synchronization) */
|
||||
val storageLow = storageLowReceiver.storageLow
|
||||
val storageLow =
|
||||
broadcastReceiverFlow(
|
||||
context = context,
|
||||
filter = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_DEVICE_STORAGE_LOW)
|
||||
addAction(Intent.ACTION_DEVICE_STORAGE_OK)
|
||||
}
|
||||
).map { intent ->
|
||||
when (intent.action) {
|
||||
Intent.ACTION_DEVICE_STORAGE_LOW -> true
|
||||
else -> false
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
/** whether global sync is disabled (sync framework won't run automatic synchronization in this case) */
|
||||
val globalSyncDisabled = MutableLiveData<Boolean>()
|
||||
|
@ -51,14 +64,26 @@ class AppWarningsModel @Inject constructor(
|
|||
/** whether a usable network connection is available (sync framework won't run synchronization otherwise) */
|
||||
val networkAvailable = MutableLiveData<Boolean>()
|
||||
private lateinit var networkCallback: ConnectivityManager.NetworkCallback
|
||||
|
||||
private val powerManager = context.getSystemService<PowerManager>()!!
|
||||
/** whether battery saver is active */
|
||||
val batterySaverActive =
|
||||
broadcastReceiverFlow(
|
||||
context = context,
|
||||
filter = IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED),
|
||||
immediate = true
|
||||
).map { powerManager.isPowerSaveMode }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
private val connectivityManager = context.getSystemService<ConnectivityManager>()!!
|
||||
|
||||
val batterySaverActive = MutableLiveData<Boolean>()
|
||||
private val batterySaverListener: BroadcastReceiver
|
||||
|
||||
/** whether data saver is restricting background synchronization ([ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED]) */
|
||||
val dataSaverEnabled = MutableLiveData<Boolean>()
|
||||
private val dataSaverChangedListener: BroadcastReceiver
|
||||
val dataSaverEnabled =
|
||||
broadcastReceiverFlow(
|
||||
context = context,
|
||||
filter = IntentFilter(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED),
|
||||
immediate = true
|
||||
).map { connectivityManager.restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
init {
|
||||
// Automatic Sync
|
||||
|
@ -67,56 +92,14 @@ class AppWarningsModel @Inject constructor(
|
|||
|
||||
// Network
|
||||
watchConnectivity()
|
||||
|
||||
// Battery saver
|
||||
batterySaverListener = object: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
checkBatterySaver()
|
||||
}
|
||||
}
|
||||
val batterySaverListenerFilter = IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)
|
||||
context.registerReceiver(batterySaverListener, batterySaverListenerFilter)
|
||||
checkBatterySaver()
|
||||
|
||||
// Data saver
|
||||
dataSaverChangedListener = object: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
checkDataSaver()
|
||||
}
|
||||
}
|
||||
val dataSaverChangedFilter = IntentFilter(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED)
|
||||
context.registerReceiver(dataSaverChangedListener, dataSaverChangedFilter)
|
||||
checkDataSaver()
|
||||
}
|
||||
|
||||
private fun checkBatterySaver() {
|
||||
batterySaverActive.postValue(
|
||||
getApplication<Application>().getSystemService<PowerManager>()?.isPowerSaveMode
|
||||
)
|
||||
}
|
||||
|
||||
private fun checkDataSaver() {
|
||||
dataSaverEnabled.postValue(
|
||||
getApplication<Application>().getSystemService<ConnectivityManager>()?.let { connectivityManager ->
|
||||
connectivityManager.restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
val context = getApplication<Application>()
|
||||
|
||||
// Automatic sync
|
||||
ContentResolver.removeStatusChangeListener(syncStatusObserver)
|
||||
|
||||
// Network
|
||||
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||
|
||||
// Battery saver
|
||||
context.unregisterReceiver(batterySaverListener)
|
||||
|
||||
// Data Saver
|
||||
context.unregisterReceiver(dataSaverChangedListener)
|
||||
}
|
||||
|
||||
override fun onStatusChanged(which: Int) {
|
||||
|
|
|
@ -35,15 +35,17 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.PackageChangedReceiver
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.ui.composable.BasicTopAppBar
|
||||
import at.bitfire.davdroid.ui.composable.CardWithImage
|
||||
import at.bitfire.davdroid.ui.composable.PermissionSwitchRow
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.util.packageChangedFlow
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.logging.Level
|
||||
|
||||
class PermissionsActivity: AppCompatActivity() {
|
||||
|
@ -81,19 +83,12 @@ class PermissionsActivity: AppCompatActivity() {
|
|||
var tasksOrgAvailable by mutableStateOf(false)
|
||||
var jtxAvailable by mutableStateOf(false)
|
||||
|
||||
private val tasksWatcher = object: PackageChangedReceiver(app) {
|
||||
override fun onPackageChanged() {
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
tasksWatcher.register()
|
||||
checkPermissions()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
tasksWatcher.close()
|
||||
viewModelScope.launch {
|
||||
packageChangedFlow(app).collect {
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
|
|
|
@ -28,9 +28,10 @@ import androidx.compose.material.SnackbarHostState
|
|||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.BiasAlignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -39,15 +40,11 @@ import androidx.compose.ui.res.painterResource
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.PackageChangedReceiver
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString
|
||||
|
@ -56,10 +53,12 @@ import at.bitfire.davdroid.ui.composable.CardWithImage
|
|||
import at.bitfire.davdroid.ui.composable.RadioWithSwitch
|
||||
import at.bitfire.davdroid.ui.widget.ClickableTextWithLink
|
||||
import at.bitfire.davdroid.util.TaskUtils
|
||||
import at.bitfire.davdroid.util.packageChangedFlow
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -113,29 +112,23 @@ class TasksActivity: AppCompatActivity() {
|
|||
settings.putBoolean(HINT_OPENTASKS_NOT_INSTALLED, false)
|
||||
}
|
||||
|
||||
val currentProvider = TaskUtils.currentProviderFlow(context, viewModelScope).asLiveData()
|
||||
val currentProvider = TaskUtils.currentProviderFlow(context, viewModelScope)
|
||||
val jtxSelected = currentProvider.map { it == TaskProvider.ProviderName.JtxBoard }
|
||||
val tasksOrgSelected = currentProvider.map { it == TaskProvider.ProviderName.TasksOrg }
|
||||
val openTasksSelected = currentProvider.map { it == TaskProvider.ProviderName.OpenTasks }
|
||||
|
||||
val jtxInstalled = MutableLiveData<Boolean>()
|
||||
val tasksOrgInstalled = MutableLiveData<Boolean>()
|
||||
val openTasksInstalled = MutableLiveData<Boolean>()
|
||||
|
||||
private val pkgChangedReceiver = object: PackageChangedReceiver(context) {
|
||||
override fun onPackageChanged() {
|
||||
jtxInstalled.postValue(isInstalled(TaskProvider.ProviderName.JtxBoard.packageName))
|
||||
tasksOrgInstalled.postValue(isInstalled(TaskProvider.ProviderName.TasksOrg.packageName))
|
||||
openTasksInstalled.postValue(isInstalled(TaskProvider.ProviderName.OpenTasks.packageName))
|
||||
}
|
||||
}
|
||||
var jtxInstalled by mutableStateOf(false)
|
||||
var tasksOrgInstalled by mutableStateOf(false)
|
||||
var openTasksInstalled by mutableStateOf(false)
|
||||
|
||||
init {
|
||||
pkgChangedReceiver.register(true)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
pkgChangedReceiver.close()
|
||||
viewModelScope.launch {
|
||||
packageChangedFlow(context).collect {
|
||||
jtxInstalled = isInstalled(TaskProvider.ProviderName.JtxBoard.packageName)
|
||||
tasksOrgInstalled = isInstalled(TaskProvider.ProviderName.TasksOrg.packageName)
|
||||
openTasksInstalled = isInstalled(TaskProvider.ProviderName.OpenTasks.packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isInstalled(packageName: String): Boolean =
|
||||
|
@ -164,14 +157,14 @@ fun TasksCard(
|
|||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val jtxInstalled by model.jtxInstalled.observeAsState(false)
|
||||
val jtxSelected by model.jtxSelected.observeAsState(false)
|
||||
val jtxInstalled = model.jtxInstalled
|
||||
val jtxSelected by model.jtxSelected.collectAsStateWithLifecycle(false)
|
||||
|
||||
val tasksOrgInstalled by model.tasksOrgInstalled.observeAsState(false)
|
||||
val tasksOrgSelected by model.tasksOrgSelected.observeAsState(false)
|
||||
val tasksOrgInstalled = model.tasksOrgInstalled
|
||||
val tasksOrgSelected by model.tasksOrgSelected.collectAsStateWithLifecycle(false)
|
||||
|
||||
val openTasksInstalled by model.openTasksInstalled.observeAsState(false)
|
||||
val openTasksSelected by model.openTasksSelected.observeAsState(false)
|
||||
val openTasksInstalled = model.openTasksInstalled
|
||||
val openTasksSelected by model.openTasksSelected.collectAsStateWithLifecycle(false)
|
||||
|
||||
val showAgain = model.showAgain.collectAsStateWithLifecycle(null).value ?: false
|
||||
|
||||
|
|
|
@ -7,8 +7,6 @@ package at.bitfire.davdroid.ui.account
|
|||
import android.Manifest
|
||||
import android.accounts.Account
|
||||
import android.app.Application
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.location.LocationManager
|
||||
|
@ -16,7 +14,6 @@ import android.os.Build
|
|||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
@ -40,7 +37,6 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|||
import androidx.compose.material.icons.automirrored.filled.Help
|
||||
import androidx.compose.material.icons.filled.CloudOff
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
|
@ -50,16 +46,21 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.Constants.withStatParams
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.ui.AppTheme
|
||||
import at.bitfire.davdroid.ui.composable.PermissionSwitchRow
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.util.broadcastReceiverFlow
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
@ -109,7 +110,7 @@ class WifiPermissionsActivity: AppCompatActivity() {
|
|||
packageManager.backgroundPermissionOptionLabel.toString()
|
||||
else
|
||||
stringResource(R.string.wifi_permissions_background_location_permission_label),
|
||||
locationServiceEnabled = model.isLocationEnabled.observeAsState(false).value
|
||||
locationServiceEnabled = model.locationEnabled.collectAsStateWithLifecycle().value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -129,9 +130,10 @@ class WifiPermissionsActivity: AppCompatActivity() {
|
|||
backgroundPermissionOptionLabel: String,
|
||||
locationServiceEnabled: Boolean
|
||||
) {
|
||||
Column(Modifier
|
||||
.padding(8.dp)
|
||||
.verticalScroll(rememberScrollState())) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(8.dp)
|
||||
.verticalScroll(rememberScrollState())) {
|
||||
Text(
|
||||
stringResource(R.string.wifi_permissions_intro),
|
||||
style = MaterialTheme.typography.body1
|
||||
|
@ -264,30 +266,11 @@ class WifiPermissionsActivity: AppCompatActivity() {
|
|||
context: Application
|
||||
): ViewModel() {
|
||||
|
||||
val isLocationEnabled: LiveData<Boolean> = object: LiveData<Boolean>() {
|
||||
val locationChangedReceiver = object: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
update()
|
||||
}
|
||||
}
|
||||
private val locationManager = context.getSystemService<LocationManager>()!!
|
||||
|
||||
override fun onActive() {
|
||||
context.registerReceiver(locationChangedReceiver, IntentFilter(LocationManager.MODE_CHANGED_ACTION))
|
||||
update()
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
context.unregisterReceiver(locationChangedReceiver)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun update() {
|
||||
context.getSystemService<LocationManager>()?.let { locationManager ->
|
||||
val locationEnabled = LocationManagerCompat.isLocationEnabled(locationManager)
|
||||
value = locationEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
val locationEnabled = broadcastReceiverFlow(context, IntentFilter(LocationManager.MODE_CHANGED_ACTION))
|
||||
.map { LocationManagerCompat.isLocationEnabled(locationManager) }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ package at.bitfire.davdroid.ui.intro
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
|
@ -47,6 +46,7 @@ import androidx.core.content.getSystemService
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.Constants
|
||||
|
@ -57,11 +57,13 @@ import at.bitfire.davdroid.ui.AppTheme
|
|||
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage.Model.Companion.HINT_AUTOSTART_PERMISSION
|
||||
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage.Model.Companion.HINT_BATTERY_OPTIMIZATIONS
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.util.broadcastReceiverFlow
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.launch
|
||||
import org.apache.commons.text.WordUtils
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
@ -180,23 +182,15 @@ class BatteryOptimizationsPage: IntroPage {
|
|||
val shouldBeExempted = MutableLiveData<Boolean>()
|
||||
val isExempted = MutableLiveData<Boolean>()
|
||||
val hintBatteryOptimizations = settings.getBooleanFlow(HINT_BATTERY_OPTIMIZATIONS)
|
||||
private val batteryOptimizationsReceiver = object: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
checkBatteryOptimizations()
|
||||
}
|
||||
}
|
||||
|
||||
val hintAutostartPermission = settings.getBooleanFlow(HINT_AUTOSTART_PERMISSION)
|
||||
|
||||
init {
|
||||
val intentFilter = IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED)
|
||||
context.registerReceiver(batteryOptimizationsReceiver, intentFilter)
|
||||
|
||||
checkBatteryOptimizations()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
context.unregisterReceiver(batteryOptimizationsReceiver)
|
||||
viewModelScope.launch {
|
||||
broadcastReceiverFlow(context, IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED)).collect {
|
||||
checkBatteryOptimizations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun checkBatteryOptimizations() {
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package at.bitfire.davdroid.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
|
||||
/**
|
||||
* Creates a flow that emits the respective [Intent] when a broadcast is received.
|
||||
*
|
||||
* @param context the context to register the receiver with
|
||||
* @param filter specifies which broadcasts shall be received
|
||||
* @param immediate if `true`, send an empty [Intent] as first value
|
||||
*
|
||||
* @return cold flow of [Intent]s
|
||||
*/
|
||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||
fun broadcastReceiverFlow(context: Context, filter: IntentFilter, immediate: Boolean = true): Flow<Intent> = callbackFlow {
|
||||
val receiver = object: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
trySend(intent)
|
||||
}
|
||||
}
|
||||
|
||||
// register receiver
|
||||
var filterDump = filter.toString()
|
||||
filter.dump({ filterDump = it }, "")
|
||||
Logger.log.fine("Registering broadcast receiver for $filterDump")
|
||||
context.registerReceiver(receiver, filter)
|
||||
|
||||
// send empty Intent as first value, if requested
|
||||
if (immediate)
|
||||
trySend(Intent())
|
||||
|
||||
// wait until flow is cancelled, then clean up
|
||||
awaitClose {
|
||||
Logger.log.fine("Unregistering broadcast receiver for $filterDump")
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a flow that emits the Intent when a package is added, changed or removed.
|
||||
*
|
||||
* @param context the context to register the receiver with
|
||||
* @param immediate if `true`, send an empty [Intent] as first value
|
||||
*
|
||||
* @return cold flow of [Intent]s
|
||||
*/
|
||||
fun packageChangedFlow(context: Context, immediate: Boolean = true): Flow<Intent> {
|
||||
val filter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
|
||||
addAction(Intent.ACTION_PACKAGE_CHANGED)
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addDataScheme("package")
|
||||
}
|
||||
return broadcastReceiverFlow(context, filter, immediate)
|
||||
}
|
|
@ -134,7 +134,7 @@ object TaskUtils {
|
|||
* Called
|
||||
*
|
||||
* - when a user explicitly selects another task app, or
|
||||
* - when there previously was no (usable) tasks app and [at.bitfire.davdroid.TasksWatcher] detected a new one.
|
||||
* - when there previously was no (usable) tasks app and [at.bitfire.davdroid.TasksAppWatcher] detected a new one.
|
||||
*/
|
||||
fun selectProvider(context: Context, selectedProvider: ProviderName?) {
|
||||
Logger.log.info("Selecting tasks app: $selectedProvider")
|
||||
|
|
Loading…
Reference in a new issue