Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 1b17124b authored by Sunik Kupfer's avatar Sunik Kupfer Committed by Ricki Hirner
Browse files

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: default avatarRicki Hirner <hirner@bitfire.at>
parent 02885947
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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)
+62 −90
Original line number Diff line number Diff line
@@ -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,7 +112,7 @@ class AccountListFragment: Fragment() {
            }
            accountAdapter.submitList(accounts)
            requireActivity().invalidateOptionsMenu()
        })
        }
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) =
@@ -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
            }
        }

        override fun onCleared() {
            accountManager.removeOnAccountsUpdatedListener(this)

            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
                networkReceiver?.let {
                    getApplication<Application>().unregisterReceiver(it)
            ContentResolver.addStatusChangeListener(
                ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE or ContentResolver.SYNC_OBSERVER_TYPE_PENDING,
                this
            )
        }

            else
                networkCallback?.let {
                    connectivityManager.unregisterNetworkCallback(it)
                }
        }
        data class AccountInfo(
            val account: Account,
            val status: SyncStatus
        )

        override fun onAccountsUpdated(newAccounts: Array<out Account>) {
            reloadAccounts()
@@ -279,11 +243,19 @@ class AccountListFragment: Fragment() {
                    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()
        }

    }

}
 No newline at end of file
+1 −56
Original line number Diff line number Diff line
@@ -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 −0
Original line number Diff line number Diff line
/***************************************************************************************************
 * 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)
        }
    }

}
 No newline at end of file