mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-10-06 19:34:23 +00:00
Rewrite AccountsActivity to M3 (#749)
* [WIP] Rewrite AccountsActivity to M3 * [WIP] AccountsScreen: FAB, previews * [WIP] Warning cards * Adapt FABs
This commit is contained in:
parent
0c748ebe73
commit
c33ea84c77
|
@ -23,6 +23,9 @@ interface ServiceDao {
|
|||
@Query("SELECT id FROM service WHERE accountName=:accountName")
|
||||
fun getIdsByAccount(accountName: String): List<Long>
|
||||
|
||||
@Query("SELECT id FROM service WHERE accountName=:accountName")
|
||||
suspend fun getIdsByAccountAsync(accountName: String): List<Long>
|
||||
|
||||
@Query("SELECT id FROM service WHERE accountName=:accountName AND type=:type")
|
||||
fun getIdByAccountAndType(accountName: String, type: String): LiveData<Long>
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ package at.bitfire.davdroid.syncadapter
|
|||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.accounts.OnAccountsUpdateListener
|
||||
import android.app.Application
|
||||
import android.content.ContentResolver
|
||||
import android.provider.CalendarContract
|
||||
|
@ -23,6 +24,8 @@ import at.bitfire.davdroid.settings.Settings
|
|||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import at.bitfire.davdroid.util.TaskUtils
|
||||
import at.bitfire.vcard4android.GroupMethod
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import java.util.logging.Level
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -39,7 +42,7 @@ class AccountRepository @Inject constructor(
|
|||
) {
|
||||
|
||||
val accountType = context.getString(R.string.account_type)
|
||||
|
||||
val accountManager = AccountManager.get(context)
|
||||
|
||||
/**
|
||||
* Creates a new main account with discovered services and enables periodic syncs with
|
||||
|
@ -142,8 +145,22 @@ class AccountRepository @Inject constructor(
|
|||
if (accountName.isEmpty())
|
||||
false
|
||||
else
|
||||
AccountManager.get(context)
|
||||
accountManager
|
||||
.getAccountsByType(accountType)
|
||||
.contains(Account(accountName, accountType))
|
||||
|
||||
|
||||
fun getAll() = accountManager.getAccountsByType(accountType)
|
||||
|
||||
fun getAllFlow() = callbackFlow<Set<Account>> {
|
||||
val listener = OnAccountsUpdateListener { accounts ->
|
||||
trySend(accounts.filter { it.type == accountType }.toSet())
|
||||
}
|
||||
accountManager.addOnAccountsUpdatedListener(listener, null, true)
|
||||
|
||||
awaitClose {
|
||||
accountManager.removeOnAccountsUpdatedListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -4,576 +4,67 @@
|
|||
|
||||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.accounts.OnAccountsUpdateListener
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.FloatingActionButton
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.IconToggleButton
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.BatterySaver
|
||||
import androidx.compose.material.icons.filled.DataSaverOn
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.NotificationsOff
|
||||
import androidx.compose.material.icons.filled.SignalCellularOff
|
||||
import androidx.compose.material.icons.filled.Storage
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material.rememberScaffoldState
|
||||
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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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
|
||||
import androidx.work.WorkQuery
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
|
||||
import at.bitfire.davdroid.syncadapter.BaseSyncWorker
|
||||
import at.bitfire.davdroid.syncadapter.OneTimeSyncWorker
|
||||
import at.bitfire.davdroid.syncadapter.SyncUtils
|
||||
import at.bitfire.davdroid.ui.account.AccountActivity
|
||||
import at.bitfire.davdroid.ui.account.progressAlpha
|
||||
import at.bitfire.davdroid.ui.composable.ActionCard
|
||||
import at.bitfire.davdroid.ui.intro.IntroActivity
|
||||
import at.bitfire.davdroid.ui.setup.LoginActivity
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.Collator
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AccountsActivity: AppCompatActivity() {
|
||||
|
||||
@Inject lateinit var accountsDrawerHandler: AccountsDrawerHandler
|
||||
|
||||
private val model by viewModels<Model>()
|
||||
private val warnings by viewModels<AppWarningsModel>()
|
||||
@Inject
|
||||
lateinit var accountsDrawerHandler: AccountsDrawerHandler
|
||||
|
||||
private val introActivityLauncher = registerForActivityResult(IntroActivity.Contract) { cancelled ->
|
||||
if (cancelled)
|
||||
finish()
|
||||
}
|
||||
|
||||
val model by viewModels<AccountsModel>()
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalPermissionsApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// use a separate thread to check whether IntroActivity should be shown
|
||||
if (savedInstanceState == null) {
|
||||
// use a separate thread to check whether IntroActivity should be shown
|
||||
// move to Model
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
if (IntroActivity.shouldShowIntroActivity(this@AccountsActivity))
|
||||
introActivityLauncher.launch(null)
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val scaffoldState = rememberScaffoldState(
|
||||
snackbarHostState = snackbarHostState
|
||||
)
|
||||
|
||||
val refreshing by remember { mutableStateOf(false) }
|
||||
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = {
|
||||
model.syncAllAccounts()
|
||||
})
|
||||
|
||||
val accounts by model.accountInfos.observeAsState()
|
||||
|
||||
M2Theme {
|
||||
Scaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
drawerContent = {
|
||||
accountsDrawerHandler.AccountsDrawer(
|
||||
snackbarHostState = snackbarHostState,
|
||||
onCloseDrawer = {
|
||||
scope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
topBar = topBar(scope, scaffoldState, accounts?.isNotEmpty() == true),
|
||||
floatingActionButton = floatingActionButton()
|
||||
) { padding ->
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.pullRefresh(
|
||||
state = pullRefreshState,
|
||||
enabled = accounts?.isNotEmpty() == true
|
||||
)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// background image
|
||||
Image(
|
||||
painterResource(R.drawable.accounts_background),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
|
||||
Column {
|
||||
val notificationsPermissionState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
rememberPermissionState(
|
||||
permission = Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// Warnings show as action cards
|
||||
SyncWarnings(
|
||||
notificationsWarning = notificationsPermissionState?.status?.isGranted == false,
|
||||
onClickPermissions = {
|
||||
startActivity(Intent(this@AccountsActivity, PermissionsActivity::class.java))
|
||||
},
|
||||
internetWarning = warnings.networkAvailable.observeAsState().value == false,
|
||||
onManageConnections = {
|
||||
val intent = Intent(Settings.ACTION_WIRELESS_SETTINGS)
|
||||
if (intent.resolveActivity(packageManager) != null)
|
||||
startActivity(intent)
|
||||
},
|
||||
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.collectAsStateWithLifecycle().value,
|
||||
onManageBatterySaver = {
|
||||
val intent = Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS)
|
||||
if (intent.resolveActivity(packageManager) != null)
|
||||
startActivity(intent)
|
||||
},
|
||||
lowStorageWarning = warnings.storageLow.collectAsStateWithLifecycle().value,
|
||||
onManageStorage = {
|
||||
val intent = Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS)
|
||||
if (intent.resolveActivity(packageManager) != null)
|
||||
startActivity(intent)
|
||||
}
|
||||
)
|
||||
|
||||
// account list
|
||||
AccountList(
|
||||
accounts = accounts ?: emptyMap(),
|
||||
onClickAccount = { account ->
|
||||
val activity = this@AccountsActivity
|
||||
val intent = Intent(activity, AccountActivity::class.java)
|
||||
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
|
||||
activity.startActivity(intent)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// indicate when the user pulls down
|
||||
PullRefreshIndicator(refreshing, pullRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
scope.launch {
|
||||
if (scaffoldState.drawerState.isOpen)
|
||||
scaffoldState.drawerState.close()
|
||||
else
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle "Sync all" intent from launcher shortcut
|
||||
if (savedInstanceState == null && intent.action == Intent.ACTION_SYNC)
|
||||
model.syncAllAccounts()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun floatingActionButton(): @Composable (() -> Unit) = {
|
||||
val show by model.showAddAccount.observeAsState()
|
||||
if (show == true)
|
||||
FloatingActionButton(onClick = {
|
||||
startActivity(Intent(this@AccountsActivity, LoginActivity::class.java))
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.Add,
|
||||
stringResource(R.string.login_create_account)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun topBar(
|
||||
scope: CoroutineScope,
|
||||
scaffoldState: ScaffoldState,
|
||||
accountsNotEmpty: Boolean
|
||||
): @Composable (() -> Unit) = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconToggleButton(false, onCheckedChange = { openDrawer ->
|
||||
scope.launch {
|
||||
if (openDrawer)
|
||||
scaffoldState.drawerState.open()
|
||||
else
|
||||
scaffoldState.drawerState.close()
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.Menu,
|
||||
stringResource(androidx.compose.ui.R.string.navigation_menu)
|
||||
)
|
||||
setContent {
|
||||
AccountsScreen(
|
||||
accountsDrawerHandler = accountsDrawerHandler,
|
||||
onAddAccount = {
|
||||
startActivity(Intent(this, LoginActivity::class.java))
|
||||
},
|
||||
onShowAccount = { account ->
|
||||
val intent = Intent(this, AccountActivity::class.java)
|
||||
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
|
||||
startActivity(intent)
|
||||
},
|
||||
onManagePermissions = {
|
||||
startActivity(Intent(this, PermissionsActivity::class.java))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(R.string.app_name))
|
||||
},
|
||||
actions = {
|
||||
if (accountsNotEmpty) {
|
||||
IconButton(onClick = { model.syncAllAccounts() }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_sync),
|
||||
contentDescription = stringResource(R.string.accounts_sync_all)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@HiltViewModel
|
||||
class Model @Inject constructor(
|
||||
application: Application,
|
||||
val db: AppDatabase
|
||||
): AndroidViewModel(application), OnAccountsUpdateListener {
|
||||
|
||||
val accountManager = AccountManager.get(application)
|
||||
private val accountType = application.getString(R.string.account_type)
|
||||
|
||||
val workManager = WorkManager.getInstance(application)
|
||||
val runningWorkers = workManager.getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING))
|
||||
|
||||
val accounts = MutableLiveData<Set<Account>>()
|
||||
val accountInfos = object: MediatorLiveData<Map<Account, AccountActivity.Progress>>() {
|
||||
var myAccounts: Set<Account> = emptySet()
|
||||
var workInfos: List<WorkInfo> = emptyList()
|
||||
init {
|
||||
addSource(accounts) { newAccounts ->
|
||||
myAccounts = newAccounts
|
||||
update()
|
||||
}
|
||||
addSource(runningWorkers) { newWorkInfos ->
|
||||
workInfos = newWorkInfos
|
||||
update()
|
||||
}
|
||||
}
|
||||
fun update() = viewModelScope.launch(Dispatchers.Default) {
|
||||
val authorities = SyncUtils.syncAuthorities(application)
|
||||
val collator = Collator.getInstance()
|
||||
postValue(myAccounts
|
||||
.sortedWith { a, b -> collator.compare(a.name, b.name) }
|
||||
.associateWith { account ->
|
||||
val services = db.serviceDao().getIdsByAccount(account.name)
|
||||
when {
|
||||
workInfos.any { info ->
|
||||
info.state == WorkInfo.State.RUNNING && (
|
||||
services.any { serviceId ->
|
||||
info.tags.contains(RefreshCollectionsWorker.workerName(serviceId))
|
||||
} || authorities.any { authority ->
|
||||
info.tags.contains(BaseSyncWorker.commonTag(account, authority))
|
||||
}
|
||||
)
|
||||
} -> AccountActivity.Progress.Active
|
||||
|
||||
workInfos.any { info ->
|
||||
info.state == WorkInfo.State.ENQUEUED && authorities.any { authority ->
|
||||
info.tags.contains(OneTimeSyncWorker.workerName(account, authority))
|
||||
}
|
||||
} -> AccountActivity.Progress.Pending
|
||||
|
||||
else -> AccountActivity.Progress.Idle
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
val showAddAccount = MutableLiveData(true)
|
||||
|
||||
init {
|
||||
accountManager.addOnAccountsUpdatedListener(this, null, true)
|
||||
}
|
||||
|
||||
// callbacks
|
||||
|
||||
override fun onAccountsUpdated(newAccounts: Array<out Account>) {
|
||||
accounts.postValue(newAccounts.filter { it.type == accountType }.toSet())
|
||||
}
|
||||
|
||||
|
||||
// actions
|
||||
|
||||
fun syncAllAccounts() {
|
||||
val context = getApplication<Application>()
|
||||
if (Build.VERSION.SDK_INT >= 25)
|
||||
context.getSystemService<ShortcutManager>()?.reportShortcutUsed(UiUtils.SHORTCUT_SYNC_ALL)
|
||||
|
||||
// Enqueue sync worker for all accounts and authorities. Will sync once internet is available
|
||||
for (account in allAccounts())
|
||||
OneTimeSyncWorker.enqueueAllAuthorities(context, account, manual = true)
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
private fun allAccounts() =
|
||||
AccountManager.get(getApplication()).getAccountsByType(accountType)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun AccountList(
|
||||
accounts: Map<Account, AccountActivity.Progress>,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickAccount: (Account) -> Unit = {}
|
||||
) {
|
||||
Column(modifier) {
|
||||
if (accounts.isEmpty())
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.account_list_empty),
|
||||
style = MaterialTheme.typography.h6,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
else
|
||||
for ((account, progress) in accounts)
|
||||
Card(
|
||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||
contentColor = MaterialTheme.colors.onSecondary,
|
||||
modifier = Modifier
|
||||
.clickable { onClickAccount(account) }
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Column {
|
||||
val progressAlpha = progressAlpha(progress)
|
||||
when (progress) {
|
||||
AccountActivity.Progress.Active ->
|
||||
LinearProgressIndicator(
|
||||
color = MaterialTheme.colors.onSecondary,
|
||||
modifier = Modifier
|
||||
.alpha(progressAlpha)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
AccountActivity.Progress.Pending,
|
||||
AccountActivity.Progress.Idle ->
|
||||
LinearProgressIndicator(
|
||||
progress = 1f,
|
||||
color = MaterialTheme.colors.onSecondary,
|
||||
modifier = Modifier
|
||||
.alpha(progressAlpha)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.AccountCircle,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.size(48.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = account.name,
|
||||
style = MaterialTheme.typography.h5,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AccountList_Preview_Idle() {
|
||||
AccountList(mapOf(
|
||||
Account("Account Name", "test") to AccountActivity.Progress.Idle
|
||||
))
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AccountList_Preview_SyncPending() {
|
||||
AccountList(mapOf(
|
||||
Account("Account Name", "test") to AccountActivity.Progress.Pending
|
||||
))
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AccountList_Preview_Syncing() {
|
||||
AccountList(mapOf(
|
||||
Account("Account Name", "test") to AccountActivity.Progress.Active
|
||||
))
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AccountList_Preview_Empty() {
|
||||
AccountList(emptyMap())
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun SyncWarnings(
|
||||
notificationsWarning: Boolean,
|
||||
onClickPermissions: () -> Unit = {},
|
||||
internetWarning: Boolean,
|
||||
onManageConnections: () -> Unit = {},
|
||||
batterySaverActive: Boolean,
|
||||
onManageBatterySaver: () -> Unit = {},
|
||||
dataSaverActive: Boolean,
|
||||
onManageDataSaver: () -> Unit = {},
|
||||
lowStorageWarning: Boolean,
|
||||
onManageStorage: () -> Unit = {}
|
||||
) {
|
||||
Column(Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
|
||||
if (notificationsWarning)
|
||||
ActionCard(
|
||||
icon = Icons.Default.NotificationsOff,
|
||||
actionText = stringResource(R.string.account_manage_permissions),
|
||||
onAction = onClickPermissions
|
||||
) {
|
||||
Text(stringResource(R.string.account_list_no_notification_permission))
|
||||
}
|
||||
|
||||
if (internetWarning)
|
||||
ActionCard(
|
||||
icon = Icons.Default.SignalCellularOff,
|
||||
actionText = stringResource(R.string.account_list_manage_connections),
|
||||
onAction = onManageConnections
|
||||
) {
|
||||
Text(stringResource(R.string.account_list_no_internet))
|
||||
}
|
||||
|
||||
if (batterySaverActive)
|
||||
ActionCard(
|
||||
icon = Icons.Default.BatterySaver,
|
||||
actionText = stringResource(R.string.account_list_manage_battery_saver),
|
||||
onAction = onManageBatterySaver
|
||||
) {
|
||||
Text(stringResource(R.string.account_list_battery_saver_enabled))
|
||||
}
|
||||
|
||||
if (dataSaverActive)
|
||||
ActionCard(
|
||||
icon = Icons.Default.DataSaverOn,
|
||||
actionText = stringResource(R.string.account_list_manage_datasaver),
|
||||
onAction = onManageDataSaver
|
||||
) {
|
||||
Text(stringResource(R.string.account_list_datasaver_enabled))
|
||||
}
|
||||
|
||||
if (lowStorageWarning)
|
||||
ActionCard(
|
||||
icon = Icons.Default.Storage,
|
||||
actionText = stringResource(R.string.account_list_manage_storage),
|
||||
onAction = onManageStorage
|
||||
) {
|
||||
Text(stringResource(R.string.account_list_low_storage))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun SyncWarnings_Preview() {
|
||||
SyncWarnings(
|
||||
notificationsWarning = true,
|
||||
internetWarning = true,
|
||||
batterySaverActive = true,
|
||||
dataSaverActive = true,
|
||||
lowStorageWarning = true
|
||||
)
|
||||
}
|
|
@ -9,34 +9,32 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.material.SnackbarResult
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Feedback
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Storage
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationDrawerItem
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
@ -47,6 +45,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
|
@ -77,7 +76,7 @@ abstract class AccountsDrawerHandler {
|
|||
snackbarHostState: SnackbarHostState,
|
||||
onCloseDrawer: () -> Unit
|
||||
) {
|
||||
Column(Modifier
|
||||
Column(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
|
@ -181,15 +180,14 @@ abstract class AccountsDrawerHandler {
|
|||
fun Header() {
|
||||
Column(
|
||||
Modifier
|
||||
.background(Color.DarkGray)
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.background(Color.DarkGray)
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Box(
|
||||
Modifier
|
||||
.background(
|
||||
color = MaterialTheme.colors.primary,
|
||||
Modifier.background(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
)
|
||||
) {
|
||||
|
@ -199,8 +197,7 @@ abstract class AccountsDrawerHandler {
|
|||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.scale(1.2f)
|
||||
.height(56.dp)
|
||||
.width(56.dp)
|
||||
.size(64.dp)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
@ -208,12 +205,12 @@ abstract class AccountsDrawerHandler {
|
|||
Text(
|
||||
stringResource(R.string.app_name),
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.body1
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.navigation_drawer_subtitle),
|
||||
color = Color.White.copy(alpha = 0.7f),
|
||||
style = MaterialTheme.typography.body2
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -222,11 +219,11 @@ abstract class AccountsDrawerHandler {
|
|||
|
||||
@Composable
|
||||
fun MenuHeading(text: String) {
|
||||
Divider(Modifier.padding(vertical = 8.dp))
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.body2,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold),
|
||||
modifier = Modifier.padding(8.dp)
|
||||
)
|
||||
}
|
||||
|
@ -241,28 +238,15 @@ abstract class AccountsDrawerHandler {
|
|||
onClick: () -> Unit
|
||||
) {
|
||||
val closeHandler = localCloseDrawerHandler.current
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = {
|
||||
onClick()
|
||||
closeHandler.closeDrawer()
|
||||
})
|
||||
.padding(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = title,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp)
|
||||
.weight(1f)
|
||||
)
|
||||
}
|
||||
NavigationDrawerItem(
|
||||
icon = { Icon(icon, contentDescription = title) },
|
||||
label = { Text(title) },
|
||||
selected = false,
|
||||
onClick = {
|
||||
onClick()
|
||||
closeHandler.closeDrawer()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
187
app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt
Normal file
187
app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt
Normal file
|
@ -0,0 +1,187 @@
|
|||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.accounts.Account
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkQuery
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
|
||||
import at.bitfire.davdroid.syncadapter.AccountRepository
|
||||
import at.bitfire.davdroid.syncadapter.BaseSyncWorker
|
||||
import at.bitfire.davdroid.syncadapter.OneTimeSyncWorker
|
||||
import at.bitfire.davdroid.syncadapter.SyncUtils
|
||||
import at.bitfire.davdroid.util.broadcastReceiverFlow
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.text.Collator
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AccountsModel @Inject constructor(
|
||||
val context: Application,
|
||||
val accountRepository: AccountRepository,
|
||||
private val db: AppDatabase
|
||||
): ViewModel() {
|
||||
|
||||
// UI state
|
||||
|
||||
/** Tri-state enum to represent active / pending / idle status */
|
||||
enum class Progress {
|
||||
Active, // syncing or refreshing
|
||||
Pending, // sync pending
|
||||
Idle
|
||||
}
|
||||
|
||||
enum class FABStyle {
|
||||
WithText,
|
||||
Standard,
|
||||
None
|
||||
}
|
||||
|
||||
data class AccountInfo(
|
||||
val name: Account,
|
||||
val progress: Progress
|
||||
)
|
||||
|
||||
private val accounts = accountRepository.getAllFlow()
|
||||
val showAddAccount: Flow<FABStyle> = accounts.map {
|
||||
if (it.isEmpty())
|
||||
FABStyle.WithText
|
||||
else
|
||||
FABStyle.Standard
|
||||
}
|
||||
val showSyncAll: Flow<Boolean> = accounts.map { it.isNotEmpty() }
|
||||
|
||||
private val workManager = WorkManager.getInstance(context)
|
||||
private val runningWorkers = workManager.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING))
|
||||
|
||||
val accountInfos: Flow<List<AccountInfo>> = combine(accounts, runningWorkers) { accounts, workInfos ->
|
||||
val authorities = SyncUtils.syncAuthorities(context)
|
||||
val collator = Collator.getInstance()
|
||||
|
||||
accounts
|
||||
.sortedWith { a, b -> collator.compare(a.name, b.name) }
|
||||
.map { account ->
|
||||
val services = db.serviceDao().getIdsByAccountAsync(account.name)
|
||||
val progress = when {
|
||||
workInfos.any { info ->
|
||||
info.state == WorkInfo.State.RUNNING && (
|
||||
services.any { serviceId ->
|
||||
info.tags.contains(RefreshCollectionsWorker.workerName(serviceId))
|
||||
} || authorities.any { authority ->
|
||||
info.tags.contains(BaseSyncWorker.commonTag(account, authority))
|
||||
}
|
||||
)
|
||||
} -> Progress.Active
|
||||
|
||||
workInfos.any { info ->
|
||||
info.state == WorkInfo.State.ENQUEUED && authorities.any { authority ->
|
||||
info.tags.contains(OneTimeSyncWorker.workerName(account, authority))
|
||||
}
|
||||
} -> Progress.Pending
|
||||
|
||||
else -> Progress.Idle
|
||||
}
|
||||
|
||||
AccountInfo(account, progress)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// warnings
|
||||
|
||||
private val connectivityManager = context.getSystemService<ConnectivityManager>()!!
|
||||
private val powerManager: PowerManager = context.getSystemService<PowerManager>()!!
|
||||
|
||||
/** whether a usable network connection is available (sync framework won't run synchronization otherwise) */
|
||||
val networkAvailable = callbackFlow<Boolean> {
|
||||
val networkCallback = 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() {
|
||||
trySend(availableNetworks.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
val networkRequest = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
.build()
|
||||
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
|
||||
|
||||
awaitClose {
|
||||
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
}
|
||||
|
||||
/** whether battery saver is active */
|
||||
val batterySaverActive =
|
||||
broadcastReceiverFlow(
|
||||
context = context,
|
||||
filter = IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED),
|
||||
immediate = true
|
||||
).map { powerManager.isPowerSaveMode }
|
||||
|
||||
/** whether data saver is restricting background synchronization ([ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED]) */
|
||||
val dataSaverEnabled =
|
||||
broadcastReceiverFlow(
|
||||
context = context,
|
||||
filter = IntentFilter(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED),
|
||||
immediate = true
|
||||
).map { connectivityManager.restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED }
|
||||
|
||||
/** whether storage is low (prevents sync framework from running synchronization) */
|
||||
val storageLow =
|
||||
broadcastReceiverFlow(
|
||||
context = context,
|
||||
filter = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_DEVICE_STORAGE_LOW)
|
||||
addAction(Intent.ACTION_DEVICE_STORAGE_OK)
|
||||
},
|
||||
immediate = false // "storage low" intent is sticky
|
||||
).map { intent ->
|
||||
when (intent.action) {
|
||||
Intent.ACTION_DEVICE_STORAGE_LOW -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// actions
|
||||
|
||||
fun syncAllAccounts() {
|
||||
if (Build.VERSION.SDK_INT >= 25)
|
||||
context.getSystemService<ShortcutManager>()?.reportShortcutUsed(UiUtils.SHORTCUT_SYNC_ALL)
|
||||
|
||||
// Enqueue sync worker for all accounts and authorities. Will sync once internet is available
|
||||
for (account in accountRepository.getAll())
|
||||
OneTimeSyncWorker.enqueueAllAuthorities(context, account, manual = true)
|
||||
}
|
||||
|
||||
}
|
515
app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsScreen.kt
Normal file
515
app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsScreen.kt
Normal file
|
@ -0,0 +1,515 @@
|
|||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.accounts.Account
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.BatterySaver
|
||||
import androidx.compose.material.icons.filled.DataSaverOn
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.NotificationsOff
|
||||
import androidx.compose.material.icons.filled.SignalCellularOff
|
||||
import androidx.compose.material.icons.filled.Storage
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconToggleButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.ui.account.progressAlpha
|
||||
import at.bitfire.davdroid.ui.composable.ActionCard
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun AccountsScreen(
|
||||
accountsDrawerHandler: AccountsDrawerHandler,
|
||||
onAddAccount: () -> Unit,
|
||||
onShowAccount: (Account) -> Unit,
|
||||
onManagePermissions: () -> Unit,
|
||||
model: AccountsModel = viewModel()
|
||||
) {
|
||||
val accounts by model.accountInfos.collectAsStateWithLifecycle(emptyList())
|
||||
val showSyncAll by model.showSyncAll.collectAsStateWithLifecycle(true)
|
||||
val showAddAccount by model.showAddAccount.collectAsStateWithLifecycle(AccountsModel.FABStyle.Standard)
|
||||
|
||||
AccountsScreen(
|
||||
accountsDrawerHandler = accountsDrawerHandler,
|
||||
accounts = accounts,
|
||||
showSyncAll = showSyncAll,
|
||||
onSyncAll = { model.syncAllAccounts() },
|
||||
showAddAccount = showAddAccount,
|
||||
onAddAccount = onAddAccount,
|
||||
onShowAccount = onShowAccount,
|
||||
onManagePermissions = onManagePermissions,
|
||||
internetUnavailable = !model.networkAvailable.collectAsStateWithLifecycle(false).value,
|
||||
batterySaverActive = model.batterySaverActive.collectAsStateWithLifecycle(false).value,
|
||||
dataSaverActive = model.dataSaverEnabled.collectAsStateWithLifecycle(false).value,
|
||||
storageLow = model.storageLow.collectAsStateWithLifecycle(false).value
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun AccountsScreen(
|
||||
accountsDrawerHandler: AccountsDrawerHandler,
|
||||
accounts: List<AccountsModel.AccountInfo>,
|
||||
showSyncAll: Boolean = true,
|
||||
onSyncAll: () -> Unit = {},
|
||||
showAddAccount: AccountsModel.FABStyle = AccountsModel.FABStyle.Standard,
|
||||
onAddAccount: () -> Unit = {},
|
||||
onShowAccount: (Account) -> Unit = {},
|
||||
onManagePermissions: () -> Unit = {},
|
||||
internetUnavailable: Boolean = false,
|
||||
batterySaverActive: Boolean = false,
|
||||
dataSaverActive: Boolean = false,
|
||||
storageLow: Boolean = false
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||
BackHandler(drawerState.isOpen) {
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
}
|
||||
}
|
||||
|
||||
val refreshState = rememberPullToRefreshState(
|
||||
enabled = { showSyncAll }
|
||||
)
|
||||
LaunchedEffect(refreshState.isRefreshing) {
|
||||
if (refreshState.isRefreshing) {
|
||||
onSyncAll()
|
||||
refreshState.endRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
AppTheme {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconToggleButton(false, onCheckedChange = { openDrawer ->
|
||||
scope.launch {
|
||||
if (openDrawer)
|
||||
drawerState.open()
|
||||
else
|
||||
drawerState.close()
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.Menu,
|
||||
stringResource(androidx.compose.ui.R.string.navigation_menu)
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(R.string.app_name))
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
if (showSyncAll)
|
||||
FloatingActionButton(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
onClick = onSyncAll
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Sync,
|
||||
contentDescription = stringResource(R.string.accounts_sync_all)
|
||||
)
|
||||
}
|
||||
|
||||
if (showAddAccount == AccountsModel.FABStyle.WithText)
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(stringResource(R.string.login_create_account)) },
|
||||
icon = { Icon(Icons.Filled.Add, stringResource(R.string.login_create_account)) },
|
||||
onClick = onAddAccount
|
||||
)
|
||||
else if (showAddAccount == AccountsModel.FABStyle.Standard)
|
||||
FloatingActionButton(
|
||||
onClick = onAddAccount,
|
||||
modifier = Modifier.padding(top = 24.dp)
|
||||
) {
|
||||
Icon(Icons.Filled.Add, stringResource(R.string.login_create_account))
|
||||
}
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) { padding ->
|
||||
Box(Modifier.padding(padding)) {
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
ModalDrawerSheet {
|
||||
accountsDrawerHandler.AccountsDrawer(
|
||||
snackbarHostState = snackbarHostState,
|
||||
onCloseDrawer = {
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(refreshState.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// background image
|
||||
Image(
|
||||
painterResource(R.drawable.accounts_background),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
|
||||
Column {
|
||||
val notificationsPermissionState =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !LocalInspectionMode.current)
|
||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
else
|
||||
null
|
||||
|
||||
// Warnings show as action cards
|
||||
val context = LocalContext.current
|
||||
SyncWarnings(
|
||||
notificationsWarning = notificationsPermissionState?.status?.isGranted == false,
|
||||
onManagePermissions = onManagePermissions,
|
||||
internetWarning = internetUnavailable,
|
||||
onManageConnections = {
|
||||
val intent = Intent(Settings.ACTION_WIRELESS_SETTINGS)
|
||||
if (intent.resolveActivity(context.packageManager) != null)
|
||||
context.startActivity(intent)
|
||||
},
|
||||
batterySaverActive = batterySaverActive,
|
||||
onManageBatterySaver = {
|
||||
val intent = Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS)
|
||||
if (intent.resolveActivity(context.packageManager) != null)
|
||||
context.startActivity(intent)
|
||||
},
|
||||
dataSaverActive = dataSaverActive,
|
||||
onManageDataSaver = {
|
||||
val intent = Intent(
|
||||
/* action = */ Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS,
|
||||
/* uri = */ Uri.parse("package:${BuildConfig.APPLICATION_ID}")
|
||||
)
|
||||
if (intent.resolveActivity(context.packageManager) != null)
|
||||
context.startActivity(intent)
|
||||
},
|
||||
lowStorageWarning = storageLow,
|
||||
onManageStorage = {
|
||||
val intent = Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS)
|
||||
if (intent.resolveActivity(context.packageManager) != null)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
)
|
||||
|
||||
// account list
|
||||
AccountList(
|
||||
accounts = accounts,
|
||||
onClickAccount = { account ->
|
||||
onShowAccount(account)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// indicate when the user pulls down
|
||||
PullToRefreshContainer(
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
state = refreshState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AccountsScreen_Preview_Empty() {
|
||||
AccountsScreen(
|
||||
accountsDrawerHandler = object: AccountsDrawerHandler() {
|
||||
@Composable
|
||||
override fun MenuEntries(snackbarHostState: SnackbarHostState) {
|
||||
Text("Menu entries")
|
||||
}
|
||||
},
|
||||
accounts = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AccountsScreen_Preview_OneAccount() {
|
||||
AccountsScreen(
|
||||
accountsDrawerHandler = object: AccountsDrawerHandler() {
|
||||
@Composable
|
||||
override fun MenuEntries(snackbarHostState: SnackbarHostState) {
|
||||
Text("Menu entries")
|
||||
}
|
||||
},
|
||||
accounts = listOf(
|
||||
AccountsModel.AccountInfo(
|
||||
Account("Account Name", "test"),
|
||||
AccountsModel.Progress.Idle
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AccountList(
|
||||
accounts: List<AccountsModel.AccountInfo>,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickAccount: (Account) -> Unit = {}
|
||||
) {
|
||||
Column(modifier) {
|
||||
if (accounts.isEmpty())
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.account_list_empty),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
else
|
||||
for ((account, progress) in accounts)
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(1.dp),
|
||||
modifier = Modifier
|
||||
.clickable { onClickAccount(account) }
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
) {
|
||||
Column {
|
||||
val progressAlpha = progressAlpha(progress)
|
||||
when (progress) {
|
||||
AccountsModel.Progress.Active ->
|
||||
LinearProgressIndicator(
|
||||
//color = MaterialTheme.colors.onSecondary,
|
||||
modifier = Modifier
|
||||
.alpha(progressAlpha)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
AccountsModel.Progress.Pending,
|
||||
AccountsModel.Progress.Idle ->
|
||||
LinearProgressIndicator(
|
||||
progress = 1f,
|
||||
//color = MaterialTheme.colors.onSecondary,
|
||||
modifier = Modifier
|
||||
.alpha(progressAlpha)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.AccountCircle,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.size(48.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = account.name,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AccountList_Preview_Idle() {
|
||||
AccountList(
|
||||
listOf(
|
||||
AccountsModel.AccountInfo(
|
||||
Account("Account Name", "test"),
|
||||
AccountsModel.Progress.Idle
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AccountList_Preview_SyncPending() {
|
||||
AccountList(listOf(
|
||||
AccountsModel.AccountInfo(
|
||||
Account("Account Name", "test"),
|
||||
AccountsModel.Progress.Pending
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AccountList_Preview_Syncing() {
|
||||
AccountList(listOf(
|
||||
AccountsModel.AccountInfo(
|
||||
Account("Account Name", "test"),
|
||||
AccountsModel.Progress.Active
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun SyncWarnings(
|
||||
notificationsWarning: Boolean = true,
|
||||
onManagePermissions: () -> Unit = {},
|
||||
internetWarning: Boolean = true,
|
||||
onManageConnections: () -> Unit = {},
|
||||
batterySaverActive: Boolean = true,
|
||||
onManageBatterySaver: () -> Unit = {},
|
||||
dataSaverActive: Boolean = true,
|
||||
onManageDataSaver: () -> Unit = {},
|
||||
lowStorageWarning: Boolean = true,
|
||||
onManageStorage: () -> Unit = {}
|
||||
) {
|
||||
Column(Modifier.padding(horizontal = 8.dp)) {
|
||||
if (notificationsWarning)
|
||||
ActionCard(
|
||||
icon = Icons.Default.NotificationsOff,
|
||||
actionText = stringResource(R.string.account_manage_permissions),
|
||||
onAction = onManagePermissions,
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.account_list_no_notification_permission))
|
||||
}
|
||||
|
||||
if (internetWarning)
|
||||
ActionCard(
|
||||
icon = Icons.Default.SignalCellularOff,
|
||||
actionText = stringResource(R.string.account_list_manage_connections),
|
||||
onAction = onManageConnections,
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.account_list_no_internet))
|
||||
}
|
||||
|
||||
if (batterySaverActive)
|
||||
ActionCard(
|
||||
icon = Icons.Default.BatterySaver,
|
||||
actionText = stringResource(R.string.account_list_manage_battery_saver),
|
||||
onAction = onManageBatterySaver,
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.account_list_battery_saver_enabled))
|
||||
}
|
||||
|
||||
if (dataSaverActive)
|
||||
ActionCard(
|
||||
icon = Icons.Default.DataSaverOn,
|
||||
actionText = stringResource(R.string.account_list_manage_datasaver),
|
||||
onAction = onManageDataSaver,
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.account_list_datasaver_enabled))
|
||||
}
|
||||
|
||||
if (lowStorageWarning)
|
||||
ActionCard(
|
||||
icon = Icons.Default.Storage,
|
||||
actionText = stringResource(R.string.account_list_manage_storage),
|
||||
onAction = onManageStorage,
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.account_list_low_storage))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun SyncWarnings_Preview() {
|
||||
SyncWarnings(
|
||||
notificationsWarning = true,
|
||||
internetWarning = true,
|
||||
batterySaverActive = true,
|
||||
dataSaverActive = true,
|
||||
lowStorageWarning = true
|
||||
)
|
||||
}
|
|
@ -498,7 +498,7 @@ class AppSettingsActivity: AppCompatActivity() {
|
|||
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
private val powerManager = context.getSystemService<PowerManager>()!!
|
||||
val batterySavingExempted = broadcastReceiverFlow(context, IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED))
|
||||
val batterySavingExempted = broadcastReceiverFlow(context, IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED), immediate = true)
|
||||
.map { powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
|
|
|
@ -1,137 +0,0 @@
|
|||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ContentResolver
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.SyncStatusObserver
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
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
|
||||
|
||||
/**
|
||||
* 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]
|
||||
* - whether data saver is turned on -> [dataSaverEnabled]
|
||||
*/
|
||||
@HiltViewModel
|
||||
class AppWarningsModel @Inject constructor(
|
||||
val context: Application
|
||||
): ViewModel(), SyncStatusObserver {
|
||||
|
||||
/** whether storage is low (prevents sync framework from running synchronization) */
|
||||
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>()
|
||||
private var syncStatusObserver: Any? = null
|
||||
|
||||
/** 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>()!!
|
||||
/** whether data saver is restricting background synchronization ([ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED]) */
|
||||
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
|
||||
syncStatusObserver = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this)
|
||||
onStatusChanged(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS)
|
||||
|
||||
// Network
|
||||
watchConnectivity()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
// Automatic sync
|
||||
ContentResolver.removeStatusChangeListener(syncStatusObserver)
|
||||
|
||||
// Network
|
||||
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
|
||||
override fun onStatusChanged(which: Int) {
|
||||
globalSyncDisabled.postValue(!ContentResolver.getMasterSyncAutomatically())
|
||||
}
|
||||
|
||||
private fun watchConnectivity() {
|
||||
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()
|
||||
networkCallback = 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, networkCallback)
|
||||
}
|
||||
|
||||
}
|
|
@ -5,7 +5,6 @@
|
|||
package at.bitfire.davdroid.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.HelpCenter
|
||||
import androidx.compose.material.icons.filled.CloudOff
|
||||
|
@ -13,6 +12,7 @@ import androidx.compose.material.icons.filled.Forum
|
|||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.VolunteerActivism
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
|
|
|
@ -80,6 +80,7 @@ import at.bitfire.davdroid.db.Collection
|
|||
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.syncadapter.OneTimeSyncWorker
|
||||
import at.bitfire.davdroid.ui.AccountsModel
|
||||
import at.bitfire.davdroid.ui.M2Theme
|
||||
import at.bitfire.davdroid.ui.PermissionsActivity
|
||||
import at.bitfire.davdroid.ui.composable.ActionCard
|
||||
|
@ -109,13 +110,6 @@ class AccountActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
/** Tri-state enum to represent active / pending / idle status */
|
||||
enum class Progress {
|
||||
Active, // syncing or refreshing
|
||||
Pending, // sync pending
|
||||
Idle
|
||||
}
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -139,10 +133,10 @@ class AccountActivity : AppCompatActivity() {
|
|||
val cardDavRefreshing by model.cardDavRefreshing.observeAsState(false)
|
||||
val cardDavSyncPending by model.cardDavSyncPending.observeAsState(false)
|
||||
val cardDavSyncing by model.cardDavSyncing.observeAsState(false)
|
||||
val cardDavProgress: Progress = when {
|
||||
cardDavRefreshing || cardDavSyncing -> Progress.Active
|
||||
cardDavSyncPending -> Progress.Pending
|
||||
else -> Progress.Idle
|
||||
val cardDavProgress: AccountsModel.Progress = when {
|
||||
cardDavRefreshing || cardDavSyncing -> AccountsModel.Progress.Active
|
||||
cardDavSyncPending -> AccountsModel.Progress.Pending
|
||||
else -> AccountsModel.Progress.Idle
|
||||
}
|
||||
val addressBooks by model.addressBooksPager.observeAsState()
|
||||
|
||||
|
@ -151,10 +145,10 @@ class AccountActivity : AppCompatActivity() {
|
|||
val calDavRefreshing by model.calDavRefreshing.observeAsState(false)
|
||||
val calDavSyncPending by model.calDavSyncPending.observeAsState(false)
|
||||
val calDavSyncing by model.calDavSyncing.observeAsState(false)
|
||||
val calDavProgress: Progress = when {
|
||||
calDavRefreshing || calDavSyncing -> Progress.Active
|
||||
calDavSyncPending -> Progress.Pending
|
||||
else -> Progress.Idle
|
||||
val calDavProgress: AccountsModel.Progress = when {
|
||||
calDavRefreshing || calDavSyncing -> AccountsModel.Progress.Active
|
||||
calDavSyncPending -> AccountsModel.Progress.Pending
|
||||
else -> AccountsModel.Progress.Idle
|
||||
}
|
||||
val calendars by model.calendarsPager.observeAsState()
|
||||
val subscriptions by model.webcalPager.observeAsState()
|
||||
|
@ -255,12 +249,12 @@ fun AccountOverview(
|
|||
onSetShowOnlyPersonal: (showOnlyPersonal: Boolean) -> Unit,
|
||||
hasCardDav: Boolean,
|
||||
canCreateAddressBook: Boolean,
|
||||
cardDavProgress: AccountActivity.Progress,
|
||||
cardDavProgress: AccountsModel.Progress,
|
||||
cardDavRefreshing: Boolean,
|
||||
addressBooks: LazyPagingItems<Collection>?,
|
||||
hasCalDav: Boolean,
|
||||
canCreateCalendar: Boolean,
|
||||
calDavProgress: AccountActivity.Progress,
|
||||
calDavProgress: AccountsModel.Progress,
|
||||
calDavRefreshing: Boolean,
|
||||
calendars: LazyPagingItems<Collection>?,
|
||||
subscriptions: LazyPagingItems<Collection>?,
|
||||
|
@ -500,12 +494,12 @@ fun AccountOverview_CardDAV_CalDAV() {
|
|||
onSetShowOnlyPersonal = {},
|
||||
hasCardDav = true,
|
||||
canCreateAddressBook = false,
|
||||
cardDavProgress = AccountActivity.Progress.Active,
|
||||
cardDavProgress = AccountsModel.Progress.Active,
|
||||
cardDavRefreshing = false,
|
||||
addressBooks = null,
|
||||
hasCalDav = true,
|
||||
canCreateCalendar = true,
|
||||
calDavProgress = AccountActivity.Progress.Pending,
|
||||
calDavProgress = AccountsModel.Progress.Pending,
|
||||
calDavRefreshing = false,
|
||||
calendars = null,
|
||||
subscriptions = null
|
||||
|
@ -695,7 +689,7 @@ fun DeleteAccountDialog(
|
|||
@Composable
|
||||
fun ServiceTab(
|
||||
requiredPermissions: List<String>,
|
||||
progress: AccountActivity.Progress,
|
||||
progress: AccountsModel.Progress,
|
||||
collections: LazyPagingItems<Collection>?,
|
||||
onUpdateCollectionSync: (collectionId: Long, sync: Boolean) -> Unit = { _, _ -> },
|
||||
onChangeForceReadOnly: (collectionId: Long, forceReadOnly: Boolean) -> Unit = { _, _ -> },
|
||||
|
@ -707,14 +701,14 @@ fun ServiceTab(
|
|||
// progress indicator
|
||||
val progressAlpha = progressAlpha(progress)
|
||||
when (progress) {
|
||||
AccountActivity.Progress.Active -> LinearProgressIndicator(
|
||||
AccountsModel.Progress.Active -> LinearProgressIndicator(
|
||||
color = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.alpha(progressAlpha)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
AccountActivity.Progress.Pending,
|
||||
AccountActivity.Progress.Idle -> LinearProgressIndicator(
|
||||
AccountsModel.Progress.Pending,
|
||||
AccountsModel.Progress.Idle -> LinearProgressIndicator(
|
||||
color = MaterialTheme.colors.secondary,
|
||||
progress = 1f,
|
||||
modifier = Modifier
|
||||
|
@ -750,12 +744,12 @@ fun ServiceTab(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun progressAlpha(progress: AccountActivity.Progress): Float {
|
||||
fun progressAlpha(progress: AccountsModel.Progress): Float {
|
||||
val progressAlpha by animateFloatAsState(
|
||||
when (progress) {
|
||||
AccountActivity.Progress.Active -> 1f
|
||||
AccountActivity.Progress.Pending -> 0.5f
|
||||
AccountActivity.Progress.Idle -> 0f
|
||||
AccountsModel.Progress.Active -> 1f
|
||||
AccountsModel.Progress.Pending -> 0.5f
|
||||
AccountsModel.Progress.Idle -> 0f
|
||||
},
|
||||
label = "progressAlpha",
|
||||
animationSpec = tween(500)
|
||||
|
|
|
@ -268,7 +268,7 @@ class WifiPermissionsActivity: AppCompatActivity() {
|
|||
|
||||
private val locationManager = context.getSystemService<LocationManager>()!!
|
||||
|
||||
val locationEnabled = broadcastReceiverFlow(context, IntentFilter(LocationManager.MODE_CHANGED_ACTION))
|
||||
val locationEnabled = broadcastReceiverFlow(context, IntentFilter(LocationManager.MODE_CHANGED_ACTION), immediate = true)
|
||||
.map { LocationManagerCompat.isLocationEnabled(locationManager) }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
|
|
|
@ -8,14 +8,14 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ProvideTextStyle
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.NotificationAdd
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -32,7 +32,6 @@ fun ActionCard(
|
|||
content: @Composable () -> Unit
|
||||
) {
|
||||
Card(Modifier
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
.fillMaxWidth()
|
||||
.then(modifier)
|
||||
) {
|
||||
|
@ -40,12 +39,15 @@ fun ActionCard(
|
|||
.padding(top = 8.dp, start = 8.dp, end = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
ProvideTextStyle(MaterialTheme.typography.body1) {
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyLarge) {
|
||||
if (icon != null)
|
||||
Row {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(icon, "", Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(end = 8.dp))
|
||||
.padding(8.dp))
|
||||
content()
|
||||
}
|
||||
else
|
||||
|
@ -53,11 +55,11 @@ fun ActionCard(
|
|||
}
|
||||
|
||||
if (actionText != null)
|
||||
TextButton(onClick = onAction) {
|
||||
Text(
|
||||
actionText.uppercase(),
|
||||
style = MaterialTheme.typography.button
|
||||
)
|
||||
OutlinedButton(
|
||||
onClick = onAction,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
) {
|
||||
Text(actionText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -186,7 +186,7 @@ class BatteryOptimizationsPage: IntroPage {
|
|||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
broadcastReceiverFlow(context, IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED)).collect {
|
||||
broadcastReceiverFlow(context, IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED), immediate = true).collect {
|
||||
checkBatteryOptimizations()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ fun broadcastReceiverFlow(
|
|||
context: Context,
|
||||
filter: IntentFilter,
|
||||
flags: Int? = null,
|
||||
immediate: Boolean = true
|
||||
immediate: Boolean
|
||||
): Flow<Intent> = callbackFlow {
|
||||
val receiver = object: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
|
|
Loading…
Reference in a new issue