mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-10-07 03:42:59 +00:00
move warning notification logic into a warning class (bitfireAT/davx5#170)
* move warning notification logic into a warning class * Move Warnings to UI package * Minor changes * Move "global sync disabled" warning to AccountListFragment, too Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
parent
02885947da
commit
1b17124b80
|
@ -28,7 +28,7 @@ class StorageLowReceiver private constructor(
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object storageLowReceiverModule {
|
||||
object StorageLowReceiverModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun storageLowReceiver(@ApplicationContext context: Context) = StorageLowReceiver(context)
|
||||
|
|
|
@ -10,18 +10,14 @@ import android.accounts.AccountManager
|
|||
import android.accounts.OnAccountsUpdateListener
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.*
|
||||
import android.content.ContentResolver
|
||||
import android.content.Intent
|
||||
import android.content.SyncStatusObserver
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
|
@ -33,23 +29,25 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.DavUtils.SyncStatus
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.StorageLowReceiver
|
||||
import at.bitfire.davdroid.databinding.AccountListBinding
|
||||
import at.bitfire.davdroid.databinding.AccountListItemBinding
|
||||
import at.bitfire.davdroid.ui.account.AccountActivity
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.text.Collator
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AccountListFragment: Fragment() {
|
||||
|
||||
@Inject lateinit var storageLowReceiver: StorageLowReceiver
|
||||
|
||||
private var _binding: AccountListBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
val model by viewModels<Model>()
|
||||
|
||||
private var syncStatusSnackbar: Snackbar? = null
|
||||
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
|
@ -64,6 +62,23 @@ class AccountListFragment: Fragment() {
|
|||
startActivity(Intent(requireActivity(), PermissionsActivity::class.java))
|
||||
}
|
||||
|
||||
model.showSyncDisabled.observe(viewLifecycleOwner) { syncDisabled ->
|
||||
if (syncDisabled) {
|
||||
val snackbar = Snackbar
|
||||
.make(view, R.string.accounts_global_sync_disabled, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.accounts_global_sync_enable) {
|
||||
ContentResolver.setMasterSyncAutomatically(true)
|
||||
}
|
||||
snackbar.show()
|
||||
syncStatusSnackbar = snackbar
|
||||
} else {
|
||||
syncStatusSnackbar?.let { snackbar ->
|
||||
snackbar.dismiss()
|
||||
syncStatusSnackbar = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
model.networkAvailable.observe(viewLifecycleOwner) { networkAvailable ->
|
||||
binding.noNetworkInfo.visibility = if (networkAvailable) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
@ -73,7 +88,7 @@ class AccountListFragment: Fragment() {
|
|||
startActivity(intent)
|
||||
}
|
||||
|
||||
storageLowReceiver.storageLow.observe(viewLifecycleOwner) { storageLow ->
|
||||
model.storageLow.observe(viewLifecycleOwner) { storageLow ->
|
||||
binding.lowStorageInfo.visibility = if (storageLow) View.VISIBLE else View.GONE
|
||||
}
|
||||
binding.manageStorage.setOnClickListener {
|
||||
|
@ -87,7 +102,7 @@ class AccountListFragment: Fragment() {
|
|||
layoutManager = LinearLayoutManager(requireActivity())
|
||||
adapter = accountAdapter
|
||||
}
|
||||
model.accounts.observe(viewLifecycleOwner, { accounts ->
|
||||
model.accounts.observe(viewLifecycleOwner) { accounts ->
|
||||
if (accounts.isEmpty()) {
|
||||
binding.list.visibility = View.GONE
|
||||
binding.empty.visibility = View.VISIBLE
|
||||
|
@ -97,11 +112,11 @@ class AccountListFragment: Fragment() {
|
|||
}
|
||||
accountAdapter.submitList(accounts)
|
||||
requireActivity().invalidateOptionsMenu()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) =
|
||||
inflater.inflate(R.menu.activity_accounts, menu)
|
||||
inflater.inflate(R.menu.activity_accounts, menu)
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
// Show "Sync all" only when there is at least one account
|
||||
|
@ -127,6 +142,7 @@ class AccountListFragment: Fragment() {
|
|||
binding.noNotificationsInfo.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
|
||||
class AccountAdapter(
|
||||
val activity: Activity
|
||||
): ListAdapter<Model.AccountInfo, AccountAdapter.ViewHolder>(
|
||||
|
@ -177,89 +193,37 @@ class AccountListFragment: Fragment() {
|
|||
}
|
||||
|
||||
|
||||
class Model(
|
||||
application: Application
|
||||
@HiltViewModel
|
||||
class Model @Inject constructor(
|
||||
application: Application,
|
||||
val warnings: AppWarningsManager
|
||||
): AndroidViewModel(application), OnAccountsUpdateListener, SyncStatusObserver {
|
||||
|
||||
data class AccountInfo(
|
||||
val account: Account,
|
||||
val status: SyncStatus
|
||||
)
|
||||
// Warnings
|
||||
val showSyncDisabled = warnings.globalSyncDisabled
|
||||
val networkAvailable = warnings.networkAvailable
|
||||
val storageLow = warnings.storageLow
|
||||
|
||||
// Accounts
|
||||
val accounts = MutableLiveData<List<AccountInfo>>()
|
||||
val syncAuthorities by lazy { DavUtils.syncAuthorities(getApplication()) }
|
||||
private val accountManager = AccountManager.get(application)!!
|
||||
private val syncAuthorities by lazy { DavUtils.syncAuthorities(application) }
|
||||
|
||||
val networkAvailable = MutableLiveData<Boolean>()
|
||||
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
||||
private var networkReceiver: BroadcastReceiver? = null
|
||||
|
||||
private val accountManager = AccountManager.get(getApplication())!!
|
||||
private val connectivityManager = application.getSystemService<ConnectivityManager>()!!
|
||||
init {
|
||||
// watch accounts
|
||||
accountManager.addOnAccountsUpdatedListener(this, null, true)
|
||||
|
||||
// watch account status
|
||||
ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE or ContentResolver.SYNC_OBSERVER_TYPE_PENDING, this)
|
||||
|
||||
// watch connectivity
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // API level <26
|
||||
networkReceiver = object: BroadcastReceiver() {
|
||||
init {
|
||||
update()
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) = update()
|
||||
|
||||
private fun update() {
|
||||
networkAvailable.postValue(connectivityManager.allNetworkInfo.any { it.isConnected })
|
||||
}
|
||||
}
|
||||
application.registerReceiver(networkReceiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
|
||||
|
||||
} else { // API level >= 26
|
||||
networkAvailable.postValue(false)
|
||||
|
||||
// check for working (e.g. WiFi after captive portal login) Internet connection
|
||||
val networkRequest = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
.build()
|
||||
val callback = object: ConnectivityManager.NetworkCallback() {
|
||||
val availableNetworks = hashSetOf<Network>()
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
availableNetworks += network
|
||||
update()
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
availableNetworks -= network
|
||||
update()
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
networkAvailable.postValue(availableNetworks.isNotEmpty())
|
||||
}
|
||||
}
|
||||
connectivityManager.registerNetworkCallback(networkRequest, callback)
|
||||
networkCallback = callback
|
||||
}
|
||||
ContentResolver.addStatusChangeListener(
|
||||
ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE or ContentResolver.SYNC_OBSERVER_TYPE_PENDING,
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountManager.removeOnAccountsUpdatedListener(this)
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
|
||||
networkReceiver?.let {
|
||||
getApplication<Application>().unregisterReceiver(it)
|
||||
}
|
||||
|
||||
else
|
||||
networkCallback?.let {
|
||||
connectivityManager.unregisterNetworkCallback(it)
|
||||
}
|
||||
}
|
||||
data class AccountInfo(
|
||||
val account: Account,
|
||||
val status: SyncStatus
|
||||
)
|
||||
|
||||
override fun onAccountsUpdated(newAccounts: Array<out Account>) {
|
||||
reloadAccounts()
|
||||
|
@ -274,16 +238,24 @@ class AccountListFragment: Fragment() {
|
|||
val collator = Collator.getInstance()
|
||||
|
||||
val sortedAccounts = accountManager
|
||||
.getAccountsByType(context.getString(R.string.account_type))
|
||||
.sortedArrayWith { a, b ->
|
||||
collator.compare(a.name, b.name)
|
||||
}
|
||||
.getAccountsByType(context.getString(R.string.account_type))
|
||||
.sortedArrayWith { a, b ->
|
||||
collator.compare(a.name, b.name)
|
||||
}
|
||||
val accountsWithInfo = sortedAccounts.map { account ->
|
||||
AccountInfo(account, DavUtils.accountSyncStatus(context, syncAuthorities, account))
|
||||
AccountInfo(
|
||||
account,
|
||||
DavUtils.accountSyncStatus(context, syncAuthorities, account)
|
||||
)
|
||||
}
|
||||
accounts.postValue(accountsWithInfo)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountManager.removeOnAccountsUpdatedListener(this)
|
||||
warnings.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -6,32 +6,22 @@ package at.bitfire.davdroid.ui
|
|||
|
||||
import android.accounts.AccountManager
|
||||
import android.app.Activity
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SyncStatusObserver
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.databinding.ActivityAccountsBinding
|
||||
import at.bitfire.davdroid.ui.intro.IntroActivity
|
||||
import at.bitfire.davdroid.ui.setup.LoginActivity
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -47,9 +37,6 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
|
|||
@Inject lateinit var accountsDrawerHandler: AccountsDrawerHandler
|
||||
|
||||
private lateinit var binding: ActivityAccountsBinding
|
||||
private val model by viewModels<Model>()
|
||||
|
||||
private var syncStatusSnackbar: Snackbar? = null
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -73,23 +60,6 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
|
|||
}
|
||||
binding.content.fab.show()
|
||||
|
||||
model.showSyncDisabled.observe(this) { syncDisabled ->
|
||||
if (syncDisabled) {
|
||||
val snackbar = Snackbar
|
||||
.make(binding.content.coordinator, R.string.accounts_global_sync_disabled, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.accounts_global_sync_enable) {
|
||||
ContentResolver.setMasterSyncAutomatically(true)
|
||||
}
|
||||
snackbar.show()
|
||||
syncStatusSnackbar = snackbar
|
||||
} else {
|
||||
syncStatusSnackbar?.let { snackbar ->
|
||||
snackbar.dismiss()
|
||||
syncStatusSnackbar = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSupportActionBar(binding.content.toolbar)
|
||||
|
||||
val toggle = ActionBarDrawerToggle(
|
||||
|
@ -144,29 +114,4 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
|
|||
DavUtils.requestSync(this, account)
|
||||
}
|
||||
|
||||
|
||||
@HiltViewModel
|
||||
class Model @Inject constructor(
|
||||
@ApplicationContext val context: Context
|
||||
): ViewModel(), SyncStatusObserver {
|
||||
|
||||
private var syncStatusObserver: Any? = null
|
||||
val showSyncDisabled = MutableLiveData(false)
|
||||
|
||||
init {
|
||||
syncStatusObserver = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this)
|
||||
onStatusChanged(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
ContentResolver.removeStatusChangeListener(syncStatusObserver)
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
override fun onStatusChanged(which: Int) {
|
||||
showSyncDisabled.postValue(!ContentResolver.getMasterSyncAutomatically())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
127
app/src/main/java/at/bitfire/davdroid/ui/AppWarningsManager.kt
Normal file
127
app/src/main/java/at/bitfire/davdroid/ui/AppWarningsManager.kt
Normal file
|
@ -0,0 +1,127 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.content.*
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import at.bitfire.davdroid.StorageLowReceiver
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Watches some conditions that result in *Warnings* that should
|
||||
* be shown to the user in the launcher activity. The variables are
|
||||
* available as LiveData so they can be directly observed in the UI.
|
||||
*
|
||||
* Currently watches:
|
||||
*
|
||||
* - whether storage is low → [storageLow]
|
||||
* - whether global sync is disabled → [globalSyncDisabled]
|
||||
* - whether a network connection is available → [networkAvailable]
|
||||
*/
|
||||
class AppWarningsManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
storageLowReceiver: StorageLowReceiver
|
||||
) : AutoCloseable, SyncStatusObserver {
|
||||
|
||||
/** whether storage is low (prevents sync framework from running synchronization) */
|
||||
val storageLow = storageLowReceiver.storageLow
|
||||
|
||||
/** whether global sync is disabled (sync framework won't run automatic synchronization in this case) */
|
||||
val globalSyncDisabled = MutableLiveData(false)
|
||||
private var syncStatusObserver: Any? = null
|
||||
|
||||
/** whether a usable network connection is available (sync framework won't run synchronization otherwise) */
|
||||
val networkAvailable = MutableLiveData<Boolean>()
|
||||
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
||||
private var networkReceiver: BroadcastReceiver? = null
|
||||
private val connectivityManager = context.getSystemService<ConnectivityManager>()!!
|
||||
|
||||
init {
|
||||
Logger.log.fine("Watching for warning conditions")
|
||||
|
||||
// Automatic sync
|
||||
syncStatusObserver = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this)
|
||||
onStatusChanged(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS)
|
||||
|
||||
// Network
|
||||
watchConnectivity()
|
||||
}
|
||||
|
||||
private fun watchConnectivity() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // API level <26
|
||||
networkReceiver = object: BroadcastReceiver() {
|
||||
init {
|
||||
update()
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) = update()
|
||||
|
||||
private fun update() {
|
||||
networkAvailable.postValue(connectivityManager.allNetworkInfo.any { it.isConnected })
|
||||
}
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
context.registerReceiver(networkReceiver,
|
||||
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
|
||||
)
|
||||
|
||||
} else { // API level >= 26
|
||||
networkAvailable.postValue(false)
|
||||
|
||||
// check for working (e.g. WiFi after captive portal login) Internet connection
|
||||
val networkRequest = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
.build()
|
||||
val callback = object: ConnectivityManager.NetworkCallback() {
|
||||
val availableNetworks = hashSetOf<Network>()
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
availableNetworks += network
|
||||
update()
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
availableNetworks -= network
|
||||
update()
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
networkAvailable.postValue(availableNetworks.isNotEmpty())
|
||||
}
|
||||
}
|
||||
connectivityManager.registerNetworkCallback(networkRequest, callback)
|
||||
networkCallback = callback
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStatusChanged(which: Int) {
|
||||
globalSyncDisabled.postValue(!ContentResolver.getMasterSyncAutomatically())
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
Logger.log.fine("Stopping watching for warning conditions")
|
||||
|
||||
// Automatic sync
|
||||
ContentResolver.removeStatusChangeListener(syncStatusObserver)
|
||||
|
||||
// Network
|
||||
networkReceiver?.let {
|
||||
context.unregisterReceiver(it)
|
||||
}
|
||||
networkCallback?.let {
|
||||
connectivityManager.unregisterNetworkCallback(it)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue