mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-23 11:39:15 +00:00
Rewrite AccountsActivity to Compose (bitfireAT/davx5#431)
--------- Signed-off-by: Arnau Mora <arnyminerz@proton.me> Co-authored-by: Arnau Mora <arnyminerz@proton.me> Co-authored-by: Sunik Kupfer <kupfer@bitfire.at>
This commit is contained in:
parent
9d739dd087
commit
518e5147fe
|
@ -158,7 +158,8 @@ dependencies {
|
|||
implementation 'androidx.compose.runtime:runtime-livedata'
|
||||
debugImplementation 'androidx.compose.ui:ui-tooling'
|
||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||
implementation 'com.google.accompanist:accompanist-themeadapter-material:0.30.1'
|
||||
implementation "com.google.accompanist:accompanist-permissions:${versions.accompanist}"
|
||||
implementation "com.google.accompanist:accompanist-themeadapter-material:${versions.accompanist}"
|
||||
|
||||
// Jetpack Room
|
||||
implementation "androidx.room:room-runtime:${versions.room}"
|
||||
|
|
|
@ -128,14 +128,14 @@ class RefreshCollectionsWorkerTest {
|
|||
@Test
|
||||
fun testRefreshCollections_enqueuesWorker() {
|
||||
val service = createTestService(Service.TYPE_CALDAV)!!
|
||||
val workerName = RefreshCollectionsWorker.refreshCollections(context, service.id)
|
||||
val workerName = RefreshCollectionsWorker.enqueue(context, service.id)
|
||||
assertTrue(workScheduledOrRunning(context, workerName))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnStopped_stopsRefreshThread() {
|
||||
val service = createTestService(Service.TYPE_CALDAV)!!
|
||||
val workerName = RefreshCollectionsWorker.refreshCollections(context, service.id)
|
||||
val workerName = RefreshCollectionsWorker.enqueue(context, service.id)
|
||||
WorkManager.getInstance(context).cancelUniqueWork(workerName)
|
||||
assertFalse(workScheduledOrRunning(context, workerName))
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ abstract class AppDatabase: RoomDatabase() {
|
|||
db.query("SELECT id FROM service", arrayOf()).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val serviceId = cursor.getLong(0)
|
||||
RefreshCollectionsWorker.refreshCollections(context, serviceId)
|
||||
RefreshCollectionsWorker.enqueue(context, serviceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,9 @@ interface ServiceDao {
|
|||
@Query("SELECT * FROM service WHERE accountName=:accountName AND type=:type")
|
||||
fun getByAccountAndType(accountName: String, type: String): Service?
|
||||
|
||||
@Query("SELECT id FROM service WHERE accountName=:accountName")
|
||||
fun getIdsByAccount(accountName: String): List<Long>
|
||||
|
||||
@Query("SELECT id FROM service WHERE accountName=:accountName AND type=:type")
|
||||
fun getIdByAccountAndType(accountName: String, type: String): LiveData<Long>
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ class RefreshCollectionsWorker @AssistedInject constructor(
|
|||
companion object {
|
||||
|
||||
const val ARG_SERVICE_ID = "serviceId"
|
||||
const val REFRESH_COLLECTIONS_WORKER_TAG = "refreshCollectionsWorker"
|
||||
const val WORKER_TAG = "refreshCollectionsWorker"
|
||||
|
||||
// Collection properties to ask for in a propfind request to the Cal- or CardDAV server
|
||||
val DAV_COLLECTION_PROPERTIES = arrayOf(
|
||||
|
@ -122,7 +122,7 @@ class RefreshCollectionsWorker @AssistedInject constructor(
|
|||
*
|
||||
* @param serviceId what service (CalDAV/CardDAV) the worker is running for
|
||||
*/
|
||||
fun workerName(serviceId: Long): String = "$REFRESH_COLLECTIONS_WORKER_TAG-$serviceId"
|
||||
fun workerName(serviceId: Long): String = "$WORKER_TAG-$serviceId"
|
||||
|
||||
/**
|
||||
* Requests immediate refresh of a given service. If not running already. this will enqueue
|
||||
|
@ -133,24 +133,23 @@ class RefreshCollectionsWorker @AssistedInject constructor(
|
|||
*
|
||||
* @throws IllegalArgumentException when there's no service with this ID
|
||||
*/
|
||||
fun refreshCollections(context: Context, serviceId: Long): String {
|
||||
if (serviceId == -1L)
|
||||
throw IllegalArgumentException("Service with ID \"$serviceId\" does not exist")
|
||||
|
||||
fun enqueue(context: Context, serviceId: Long): String {
|
||||
val name = workerName(serviceId)
|
||||
val arguments = Data.Builder()
|
||||
.putLong(ARG_SERVICE_ID, serviceId)
|
||||
.build()
|
||||
val workRequest = OneTimeWorkRequestBuilder<RefreshCollectionsWorker>()
|
||||
.addTag(name)
|
||||
.setInputData(arguments)
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(
|
||||
workerName(serviceId),
|
||||
name,
|
||||
ExistingWorkPolicy.KEEP, // if refresh is already running, just continue that one
|
||||
workRequest
|
||||
)
|
||||
return workerName(serviceId)
|
||||
return name
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -160,7 +159,7 @@ class RefreshCollectionsWorker @AssistedInject constructor(
|
|||
* @param workState state of worker to match
|
||||
* @return boolean true if worker with matching state was found
|
||||
*/
|
||||
fun isWorkerInState(context: Context, workerName: String, workState: WorkInfo.State) =
|
||||
fun exists(context: Context, workerName: String, workState: WorkInfo.State = WorkInfo.State.RUNNING) =
|
||||
WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData(workerName).map {
|
||||
workInfoList -> workInfoList.any { workInfo -> workInfo.state == workState }
|
||||
}
|
||||
|
|
|
@ -1,339 +0,0 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.accounts.OnAccountsUpdateListener
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.MenuHost
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkQuery
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.databinding.AccountListBinding
|
||||
import at.bitfire.davdroid.databinding.AccountListItemBinding
|
||||
import at.bitfire.davdroid.syncadapter.SyncUtils.syncAuthorities
|
||||
import at.bitfire.davdroid.syncadapter.SyncWorker
|
||||
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() {
|
||||
|
||||
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 {
|
||||
_binding = AccountListBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.allowNotifications.setOnClickListener {
|
||||
startActivity(Intent(requireActivity(), PermissionsActivity::class.java))
|
||||
}
|
||||
|
||||
model.globalSyncDisabled.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
|
||||
}
|
||||
binding.manageConnections.setOnClickListener {
|
||||
val intent = Intent(Settings.ACTION_WIRELESS_SETTINGS)
|
||||
if (intent.resolveActivity(requireActivity().packageManager) != null)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
model.storageLow.observe(viewLifecycleOwner) { storageLow ->
|
||||
binding.lowStorageInfo.visibility = if (storageLow) View.VISIBLE else View.GONE
|
||||
}
|
||||
binding.manageStorage.setOnClickListener {
|
||||
val intent = Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS)
|
||||
if (intent.resolveActivity(requireActivity().packageManager) != null)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
model.dataSaverOn.observe(viewLifecycleOwner) { datasaverOn ->
|
||||
binding.datasaverOnInfo.visibility = if (datasaverOn) View.VISIBLE else View.GONE
|
||||
}
|
||||
binding.manageDatasaver.setOnClickListener {
|
||||
val intent = Intent(Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS, Uri.parse("package:" + requireActivity().packageName))
|
||||
if (intent.resolveActivity(requireActivity().packageManager) != null)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
// Accounts adapter
|
||||
val accountAdapter = AccountAdapter(requireActivity())
|
||||
binding.list.apply {
|
||||
layoutManager = LinearLayoutManager(requireActivity())
|
||||
adapter = accountAdapter
|
||||
}
|
||||
model.accounts.observe(viewLifecycleOwner) { accounts ->
|
||||
if (accounts.isEmpty()) {
|
||||
binding.list.visibility = View.GONE
|
||||
binding.empty.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.list.visibility = View.VISIBLE
|
||||
binding.empty.visibility = View.GONE
|
||||
}
|
||||
accountAdapter.submitList(accounts)
|
||||
requireActivity().invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
requireActivity().addMenuProvider(object : MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.activity_accounts, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem) =
|
||||
when (menuItem.itemId) {
|
||||
R.id.syncAll -> {
|
||||
(activity as AccountsActivity).syncAllAccounts()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
// Show "Sync all" only when there is at least one account
|
||||
model.accounts.value?.let { accounts ->
|
||||
menu.findItem(R.id.syncAll).setVisible(accounts.isNotEmpty())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
checkPermissions()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
fun checkPermissions() {
|
||||
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED)
|
||||
binding.noNotificationsInfo.visibility = View.GONE
|
||||
else
|
||||
binding.noNotificationsInfo.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
|
||||
class AccountAdapter(
|
||||
val activity: Activity
|
||||
): ListAdapter<Model.AccountInfo, AccountAdapter.ViewHolder>(
|
||||
object: DiffUtil.ItemCallback<Model.AccountInfo>() {
|
||||
override fun areItemsTheSame(oldItem: Model.AccountInfo, newItem: Model.AccountInfo) =
|
||||
oldItem.account == newItem.account
|
||||
override fun areContentsTheSame(oldItem: Model.AccountInfo, newItem: Model.AccountInfo) =
|
||||
oldItem == newItem
|
||||
}
|
||||
) {
|
||||
|
||||
class ViewHolder(val binding: AccountListItemBinding): RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding = AccountListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val accountInfo = currentList[position]
|
||||
|
||||
holder.binding.root.setOnClickListener {
|
||||
val intent = Intent(activity, AccountActivity::class.java)
|
||||
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, accountInfo.account)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
when (accountInfo.status) {
|
||||
SyncStatus.ACTIVE -> {
|
||||
holder.binding.progress.apply {
|
||||
alpha = 1.0f
|
||||
isIndeterminate = true
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
SyncStatus.PENDING -> {
|
||||
holder.binding.progress.apply {
|
||||
alpha = 0.4f
|
||||
isIndeterminate = false
|
||||
progress = 100
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
else -> holder.binding.progress.visibility = View.INVISIBLE
|
||||
}
|
||||
holder.binding.accountName.text = accountInfo.account.name
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@HiltViewModel
|
||||
class Model @Inject constructor(
|
||||
application: Application,
|
||||
private val warnings: AppWarningsManager
|
||||
): AndroidViewModel(application), OnAccountsUpdateListener {
|
||||
|
||||
data class AccountInfo(
|
||||
val account: Account,
|
||||
val status: SyncStatus
|
||||
)
|
||||
|
||||
// Warnings
|
||||
val globalSyncDisabled = warnings.globalSyncDisabled
|
||||
val dataSaverOn = warnings.dataSaverEnabled
|
||||
val networkAvailable = warnings.networkAvailable
|
||||
val storageLow = warnings.storageLow
|
||||
|
||||
// Accounts
|
||||
private val accountsUpdated = MutableLiveData<Boolean>()
|
||||
private val syncWorkersActive = SyncWorker.exists(
|
||||
application,
|
||||
listOf(
|
||||
WorkInfo.State.RUNNING,
|
||||
WorkInfo.State.ENQUEUED
|
||||
)
|
||||
)
|
||||
|
||||
val accounts = object : MediatorLiveData<List<AccountInfo>>() {
|
||||
init {
|
||||
addSource(accountsUpdated) { recalculate() }
|
||||
addSource(syncWorkersActive) { recalculate() }
|
||||
}
|
||||
|
||||
fun recalculate() {
|
||||
val context = getApplication<Application>()
|
||||
val collator = Collator.getInstance()
|
||||
|
||||
val sortedAccounts = accountManager
|
||||
.getAccountsByType(context.getString(R.string.account_type))
|
||||
.sortedArrayWith { a, b ->
|
||||
collator.compare(a.name, b.name)
|
||||
}
|
||||
val accountsWithInfo = sortedAccounts.map { account ->
|
||||
AccountInfo(
|
||||
account,
|
||||
SyncStatus.fromAccount(context, account)
|
||||
)
|
||||
}
|
||||
value = accountsWithInfo
|
||||
}
|
||||
}
|
||||
|
||||
private val accountManager = AccountManager.get(application)!!
|
||||
|
||||
init {
|
||||
// watch accounts
|
||||
accountManager.addOnAccountsUpdatedListener(this, null, true)
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
override fun onAccountsUpdated(newAccounts: Array<out Account>) {
|
||||
accountsUpdated.postValue(true)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountManager.removeOnAccountsUpdatedListener(this)
|
||||
warnings.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum class SyncStatus {
|
||||
ACTIVE, PENDING, IDLE;
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Returns the sync status of a given account. Checks the account itself and possible
|
||||
* sub-accounts (address book accounts).
|
||||
*
|
||||
* @param account account to check
|
||||
*
|
||||
* @return sync status of the given account
|
||||
*/
|
||||
fun fromAccount(context: Context, account: Account): SyncStatus {
|
||||
// Add contacts authority, so sync status of address-book-accounts is also checked
|
||||
val workerNames = syncAuthorities(context, true).map { authority ->
|
||||
SyncWorker.workerName(account, authority)
|
||||
}
|
||||
val workQuery = WorkQuery.Builder
|
||||
.fromTags(workerNames)
|
||||
.addStates(listOf(WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED))
|
||||
.build()
|
||||
|
||||
val workInfos = WorkManager.getInstance(context).getWorkInfos(workQuery).get()
|
||||
|
||||
return when {
|
||||
workInfos.any { workInfo ->
|
||||
workInfo.state == WorkInfo.State.RUNNING
|
||||
} -> ACTIVE
|
||||
|
||||
workInfos.any {workInfo ->
|
||||
workInfo.state == WorkInfo.State.ENQUEUED
|
||||
} -> PENDING
|
||||
|
||||
else -> IDLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -4,52 +4,115 @@
|
|||
|
||||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.app.Activity
|
||||
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.view.MenuItem
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.TooltipCompat
|
||||
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.ColumnScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
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.ProgressIndicatorDefaults
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.material.SnackbarHost
|
||||
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.Menu
|
||||
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.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.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
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.databinding.ActivityAccountsBinding
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
|
||||
import at.bitfire.davdroid.syncadapter.SyncUtils
|
||||
import at.bitfire.davdroid.syncadapter.SyncWorker
|
||||
import at.bitfire.davdroid.ui.account.AccountActivity
|
||||
import at.bitfire.davdroid.ui.intro.IntroActivity
|
||||
import at.bitfire.davdroid.ui.setup.LoginActivity
|
||||
import at.bitfire.davdroid.ui.widget.ActionCard
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.Collator
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
|
||||
class AccountsActivity: AppCompatActivity() {
|
||||
|
||||
@Inject lateinit var accountsDrawerHandler: AccountsDrawerHandler
|
||||
|
||||
private lateinit var binding: ActivityAccountsBinding
|
||||
val model by viewModels<Model>()
|
||||
private val model by viewModels<Model>()
|
||||
|
||||
private val introActivityLauncher = registerForActivityResult(IntroActivity.Contract) { cancelled ->
|
||||
if (cancelled) {
|
||||
if (cancelled)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalPermissionsApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
@ -61,79 +124,462 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
|
|||
}
|
||||
}
|
||||
|
||||
binding = ActivityAccountsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setContent {
|
||||
val scope = rememberCoroutineScope()
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
TooltipCompat.setTooltipText(binding.content.fab, binding.content.fab.contentDescription)
|
||||
binding.content.fab.setOnClickListener {
|
||||
startActivity(Intent(this, LoginActivity::class.java))
|
||||
}
|
||||
binding.content.fab.show()
|
||||
val refreshing by remember { mutableStateOf(false) }
|
||||
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = {
|
||||
model.syncAllAccounts()
|
||||
})
|
||||
|
||||
setSupportActionBar(binding.content.toolbar)
|
||||
MdcTheme {
|
||||
Scaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
drawerContent = drawerContent(scope, scaffoldState),
|
||||
topBar = topBar(scope, scaffoldState),
|
||||
floatingActionButton = floatingActionButton(),
|
||||
snackbarHost = snackbarHost(snackbarHostState, scope)
|
||||
) { padding ->
|
||||
Box(
|
||||
Modifier
|
||||
.padding(padding)
|
||||
.pullRefresh(pullRefreshState)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
|
||||
val toggle = ActionBarDrawerToggle(
|
||||
this, binding.drawerLayout, binding.content.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
|
||||
binding.drawerLayout.addDrawerListener(toggle)
|
||||
toggle.syncState()
|
||||
// background image
|
||||
Image(
|
||||
painterResource(R.drawable.accounts_background),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
binding.navView.setNavigationItemSelectedListener(this)
|
||||
binding.navView.itemIconTintList = null
|
||||
Column {
|
||||
val warnings = model.warnings
|
||||
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
} else {
|
||||
finish()
|
||||
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(android.provider.Settings.ACTION_WIRELESS_SETTINGS)
|
||||
if (intent.resolveActivity(packageManager) != null)
|
||||
startActivity(intent)
|
||||
},
|
||||
lowStorageWarning = warnings.storageLow.observeAsState().value == true,
|
||||
onManageStorage = {
|
||||
val intent = Intent(android.provider.Settings.ACTION_INTERNAL_STORAGE_SETTINGS)
|
||||
if (intent.resolveActivity(packageManager) != null)
|
||||
startActivity(intent)
|
||||
},
|
||||
dataSaverActive = warnings.dataSaverEnabled.observeAsState().value == true,
|
||||
onManageDataSaver = {
|
||||
val intent = Intent(android.provider.Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS, Uri.parse("package:" + packageName))
|
||||
if (intent.resolveActivity(packageManager) != null)
|
||||
startActivity(intent)
|
||||
}
|
||||
)
|
||||
|
||||
// account list
|
||||
val accounts = model.accountInfos.observeAsState()
|
||||
AccountList(
|
||||
accounts = accounts.value ?: emptyList(),
|
||||
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)
|
||||
syncAllAccounts()
|
||||
model.syncAllAccounts()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
accountsDrawerHandler.initMenu(this, binding.navView.menu)
|
||||
@Composable
|
||||
private fun snackbarHost(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
scope: CoroutineScope
|
||||
): @Composable (SnackbarHostState) -> Unit = {
|
||||
SnackbarHost(snackbarHostState)
|
||||
model.feedback.observeAsState().value?.let { msg ->
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(msg)
|
||||
}
|
||||
// reset feedback
|
||||
model.feedback.value = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||
accountsDrawerHandler.onNavigationItemSelected(this, item)
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
return true
|
||||
@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
|
||||
): @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)
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(R.string.app_name))
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { model.syncAllAccounts() }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_sync),
|
||||
contentDescription = stringResource(R.string.accounts_sync_all)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun allAccounts() =
|
||||
AccountManager.get(this).getAccountsByType(getString(R.string.account_type))
|
||||
@Composable
|
||||
private fun drawerContent(
|
||||
scope: CoroutineScope,
|
||||
scaffoldState: ScaffoldState
|
||||
): @Composable (ColumnScope.() -> Unit) =
|
||||
{
|
||||
AndroidView(factory = { context ->
|
||||
// use legacy NavigationView for now
|
||||
NavigationView(context).apply {
|
||||
inflateHeaderView(R.layout.nav_header_accounts)
|
||||
|
||||
fun syncAllAccounts() {
|
||||
if (Build.VERSION.SDK_INT >= 25)
|
||||
getSystemService<ShortcutManager>()?.reportShortcutUsed(UiUtils.SHORTCUT_SYNC_ALL)
|
||||
inflateMenu(R.menu.activity_accounts_drawer)
|
||||
accountsDrawerHandler.initMenu(this@AccountsActivity, menu)
|
||||
|
||||
// Notify user that sync will get enqueued if we're not connected to the internet
|
||||
model.networkAvailable.value?.let { networkAvailable ->
|
||||
if (!networkAvailable)
|
||||
Snackbar.make(binding.drawerLayout, R.string.no_internet_sync_scheduled, Snackbar.LENGTH_LONG).show()
|
||||
setNavigationItemSelectedListener { item ->
|
||||
scope.launch {
|
||||
accountsDrawerHandler.onNavigationItemSelected(
|
||||
this@AccountsActivity,
|
||||
item
|
||||
)
|
||||
scaffoldState.drawerState.close()
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
|
||||
// Enqueue sync worker for all accounts and authorities. Will sync once internet is available
|
||||
val accounts = allAccounts()
|
||||
for (account in accounts)
|
||||
SyncWorker.enqueueAllAuthorities(this, account)
|
||||
}
|
||||
|
||||
data class AccountInfo(
|
||||
val account: Account,
|
||||
val isRefreshing: Boolean,
|
||||
val isSyncing: Boolean
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class Model @Inject constructor(
|
||||
application: Application,
|
||||
val settings: SettingsManager,
|
||||
warnings: AppWarningsManager
|
||||
): AndroidViewModel(application) {
|
||||
val db: AppDatabase,
|
||||
val warnings: AppWarningsManager
|
||||
): AndroidViewModel(application), OnAccountsUpdateListener {
|
||||
|
||||
val feedback = MutableLiveData<String>()
|
||||
|
||||
val accountManager = AccountManager.get(application)
|
||||
private val accountType = application.getString(R.string.account_type)
|
||||
val showAddAccount = MutableLiveData<Boolean>(true)
|
||||
|
||||
val workManager = WorkManager.getInstance(application)
|
||||
val runningWorkers = workManager.getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.RUNNING))
|
||||
|
||||
val accounts = MutableLiveData<Set<Account>>()
|
||||
val accountInfos = object: MediatorLiveData<List<AccountInfo>>() {
|
||||
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, withContacts = true)
|
||||
val collator = Collator.getInstance()
|
||||
postValue(myAccounts
|
||||
.toList()
|
||||
.sortedWith { a, b -> collator.compare(a.name, b.name) }
|
||||
.map { account ->
|
||||
val services = db.serviceDao().getIdsByAccount(account.name)
|
||||
AccountInfo(
|
||||
account = account,
|
||||
isRefreshing = workInfos.any { info ->
|
||||
services.any { serviceId ->
|
||||
info.tags.contains(RefreshCollectionsWorker.workerName(serviceId))
|
||||
}
|
||||
},
|
||||
isSyncing = workInfos.any { info ->
|
||||
authorities.any { authority ->
|
||||
info.tags.contains(SyncWorker.workerName(account, authority))
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
val networkAvailable = warnings.networkAvailable
|
||||
|
||||
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)
|
||||
|
||||
feedback.value = context.getString(
|
||||
if (networkAvailable.value == false)
|
||||
R.string.no_internet_sync_scheduled
|
||||
else
|
||||
R.string.sync_started
|
||||
)
|
||||
|
||||
// Enqueue sync worker for all accounts and authorities. Will sync once internet is available
|
||||
for (account in allAccounts())
|
||||
SyncWorker.enqueueAllAuthorities(context, account)
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
private fun allAccounts() =
|
||||
AccountManager.get(getApplication()).getAccountsByType(accountType)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun AccountList(
|
||||
accounts: List<AccountsActivity.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.h6,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
else
|
||||
for (account in accounts)
|
||||
Card(
|
||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||
contentColor = MaterialTheme.colors.onSecondary,
|
||||
modifier = Modifier
|
||||
.clickable { onClickAccount(account.account) }
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Column {
|
||||
if (account.isRefreshing || account.isSyncing)
|
||||
LinearProgressIndicator(
|
||||
color = MaterialTheme.colors.onSecondary,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
else
|
||||
Spacer(Modifier.height(ProgressIndicatorDefaults.StrokeWidth))
|
||||
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.AccountCircle,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.size(48.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = account.account.name,
|
||||
style = MaterialTheme.typography.h5,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AccountList_Preview_Idle() {
|
||||
AccountList(listOf(
|
||||
AccountsActivity.AccountInfo(
|
||||
Account("Account Name", "test"),
|
||||
isRefreshing = false,
|
||||
isSyncing = false
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AccountList_Preview_IsSyncing() {
|
||||
AccountList(listOf(
|
||||
AccountsActivity.AccountInfo(
|
||||
Account("Account Name", "test"),
|
||||
isRefreshing = false,
|
||||
isSyncing = true
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AccountList_Preview_Empty() {
|
||||
AccountList(listOf())
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun SyncWarnings(
|
||||
notificationsWarning: Boolean,
|
||||
onClickPermissions: () -> Unit = {},
|
||||
internetWarning: Boolean,
|
||||
onManageConnections: () -> Unit = {},
|
||||
lowStorageWarning: Boolean,
|
||||
onManageStorage: () -> Unit = {},
|
||||
dataSaverActive: Boolean,
|
||||
onManageDataSaver: () -> Unit = {}
|
||||
) {
|
||||
Column(Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
|
||||
if (notificationsWarning)
|
||||
ActionCard(
|
||||
icon = painterResource(R.drawable.ic_notifications_off),
|
||||
actionText = stringResource(R.string.account_permissions_action),
|
||||
onAction = onClickPermissions
|
||||
) {
|
||||
Text(stringResource(R.string.account_list_no_notification_permission))
|
||||
}
|
||||
|
||||
if (internetWarning)
|
||||
ActionCard(
|
||||
icon = painterResource(R.drawable.ic_signal_cellular_off),
|
||||
actionText = stringResource(R.string.account_list_manage_connections),
|
||||
onAction = onManageConnections
|
||||
) {
|
||||
Text(stringResource(R.string.account_list_no_internet))
|
||||
}
|
||||
|
||||
if (lowStorageWarning)
|
||||
ActionCard(
|
||||
icon = painterResource(R.drawable.ic_storage),
|
||||
actionText = stringResource(R.string.account_list_manage_storage),
|
||||
onAction = onManageStorage
|
||||
) {
|
||||
Text(stringResource(R.string.account_list_low_storage))
|
||||
}
|
||||
|
||||
if (dataSaverActive)
|
||||
ActionCard(
|
||||
icon = painterResource(R.drawable.ic_datasaver_on),
|
||||
actionText = stringResource(R.string.account_list_manage_datasaver),
|
||||
onAction = onManageDataSaver
|
||||
) {
|
||||
Text(stringResource(R.string.account_list_datasaver_enabled))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun SyncWarnings_Preview() {
|
||||
SyncWarnings(
|
||||
notificationsWarning = true,
|
||||
internetWarning = true,
|
||||
lowStorageWarning = true,
|
||||
dataSaverActive = true
|
||||
)
|
||||
}
|
||||
|
|
|
@ -13,9 +13,9 @@ import android.view.ViewGroup
|
|||
import android.widget.ArrayAdapter
|
||||
import android.widget.Filter
|
||||
import android.widget.TextView
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.HomeSet
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
|
||||
class HomeSetAdapter(
|
||||
context: Context
|
||||
|
|
|
@ -5,17 +5,14 @@
|
|||
package at.bitfire.davdroid.ui.account
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.core.view.MenuHost
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.databinding.AccountCarddavItemBinding
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.settings.Settings
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
|
|
|
@ -5,17 +5,19 @@
|
|||
package at.bitfire.davdroid.ui.account
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.core.view.MenuHost
|
||||
import androidx.core.view.MenuProvider
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.databinding.AccountCaldavItemBinding
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.resource.TaskUtils
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
|
||||
class CalendarsFragment: CollectionsFragment() {
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ import android.provider.ContactsContract
|
|||
import android.view.*
|
||||
import android.widget.PopupMenu
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.core.view.MenuHost
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
|
@ -315,7 +314,7 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
|
|||
}
|
||||
|
||||
// observe RefreshCollectionsWorker status
|
||||
val isRefreshing = RefreshCollectionsWorker.isWorkerInState(getApplication(), RefreshCollectionsWorker.workerName(serviceId), WorkInfo.State.RUNNING)
|
||||
val isRefreshing = RefreshCollectionsWorker.exists(getApplication(), RefreshCollectionsWorker.workerName(serviceId))
|
||||
|
||||
// observe SyncWorker state
|
||||
private val authorities =
|
||||
|
@ -335,7 +334,7 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
|
|||
// actions
|
||||
|
||||
fun refresh() {
|
||||
RefreshCollectionsWorker.refreshCollections(getApplication(), serviceId)
|
||||
RefreshCollectionsWorker.enqueue(getApplication(), serviceId)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -147,7 +147,7 @@ class CreateCollectionFragment: DialogFragment() {
|
|||
db.collectionDao().insert(collection)
|
||||
|
||||
// trigger service detection (because the collection may have other properties than the ones we have inserted)
|
||||
RefreshCollectionsWorker.refreshCollections(getApplication(), service.id)
|
||||
RefreshCollectionsWorker.enqueue(getApplication(), service.id)
|
||||
}
|
||||
|
||||
// post success
|
||||
|
|
|
@ -8,8 +8,8 @@ import android.app.Activity
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
|
@ -21,7 +21,6 @@ import dagger.hilt.InstallIn
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class IntroActivity: AppIntro2() {
|
||||
|
|
|
@ -10,10 +10,10 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.util.PermissionUtils.CALENDAR_PERMISSIONS
|
||||
import at.bitfire.davdroid.util.PermissionUtils.CONTACT_PERMISSIONS
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
|
@ -212,7 +212,7 @@ class AccountDetailsFragment : Fragment() {
|
|||
accountSettings.setGroupMethod(groupMethod)
|
||||
|
||||
// start CardDAV service detection (refresh collections)
|
||||
RefreshCollectionsWorker.refreshCollections(context, id)
|
||||
RefreshCollectionsWorker.enqueue(context, id)
|
||||
|
||||
// set default sync interval and enable sync regardless of permissions
|
||||
ContentResolver.setIsSyncable(account, addrBookAuthority, 1)
|
||||
|
@ -226,7 +226,7 @@ class AccountDetailsFragment : Fragment() {
|
|||
val id = insertService(name, Service.TYPE_CALDAV, config.calDAV)
|
||||
|
||||
// start CalDAV service detection (refresh collections)
|
||||
RefreshCollectionsWorker.refreshCollections(context, id)
|
||||
RefreshCollectionsWorker.enqueue(context, id)
|
||||
|
||||
// set default sync interval and enable sync regardless of permissions
|
||||
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1)
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.ui.widget
|
||||
|
||||
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.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.bitfire.davdroid.R
|
||||
|
||||
@Composable
|
||||
fun ActionCard(
|
||||
icon: Painter? = null,
|
||||
actionText: String? = null,
|
||||
onAction: () -> Unit = {},
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Card(Modifier
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(Modifier.padding(top = 8.dp, start = 8.dp, end = 8.dp)) {
|
||||
if (icon != null)
|
||||
Row {
|
||||
Icon(icon, "", Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(end = 8.dp))
|
||||
content()
|
||||
}
|
||||
else
|
||||
content()
|
||||
|
||||
if (actionText != null)
|
||||
TextButton(onClick = onAction) {
|
||||
Text(
|
||||
actionText.uppercase(),
|
||||
style = MaterialTheme.typography.button
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun ActionCard_Sample() {
|
||||
ActionCard(
|
||||
icon = painterResource(R.drawable.ic_notifications_off),
|
||||
actionText = "Some Action"
|
||||
) {
|
||||
Text("Some Content")
|
||||
}
|
||||
}
|
|
@ -1,178 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/no_notifications_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:contentPadding="@dimen/card_padding">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
app:drawableLeftCompat="@drawable/ic_notifications_off"
|
||||
app:drawableTint="?android:attr/textColorPrimary"
|
||||
android:drawablePadding="8dp"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:text="@string/account_list_no_notification_permission"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/allow_notifications"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:text="@string/account_permissions_action" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/no_network_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:contentPadding="@dimen/card_padding">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
app:drawableLeftCompat="@drawable/ic_signal_cellular_off"
|
||||
app:drawableTint="?android:attr/textColorPrimary"
|
||||
android:drawablePadding="8dp"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:text="@string/account_list_no_internet"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/manage_connections"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:text="@string/account_list_manage_connections" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/low_storage_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:contentPadding="@dimen/card_padding">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:visibility="visible"
|
||||
app:drawableLeftCompat="@drawable/ic_storage"
|
||||
app:drawableTint="?android:attr/textColorPrimary"
|
||||
android:drawablePadding="8dp"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:text="@string/account_list_low_storage"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/manage_storage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:text="@string/account_list_manage_storage" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/datasaver_on_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:contentPadding="@dimen/card_padding">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:visibility="visible"
|
||||
app:drawableLeftCompat="@drawable/ic_datasaver_on"
|
||||
app:drawableTint="?android:attr/textColorPrimary"
|
||||
android:drawablePadding="8dp"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:text="@string/account_list_datasaver_enabled"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/manage_datasaver"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:text="@string/account_list_manage_datasaver" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@android:id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:divider="@android:color/transparent"
|
||||
android:background="@android:color/transparent"
|
||||
android:cacheColorHint="@android:color/transparent" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@android:id/empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/text"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias=".5"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:adjustViewBounds="true"
|
||||
app:srcCompat="@drawable/accounts_background"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/image"
|
||||
android:gravity="center"
|
||||
style="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
android:text="@string/account_list_empty" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
|
@ -1,57 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:descendantFocusability="blocksDescendants">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
style="@style/account_list_card"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
app:cardBackgroundColor="@color/cardview_background"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:progressTint="?attr/colorOnSecondary"
|
||||
android:indeterminate="true"
|
||||
android:indeterminateTint="?attr/colorOnSecondary"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:tint="?attr/colorOnSecondary"
|
||||
app:srcCompat="@drawable/ic_account_circle_white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
||||
android:textColor="?attr/colorOnSecondary"
|
||||
tools:text="Account Name"/>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</LinearLayout>
|
|
@ -1,41 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/coordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="at.bitfire.davdroid.ui.AccountsActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:theme="?attr/actionBarTheme"
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"/>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/account_list"
|
||||
android:name="at.bitfire.davdroid.ui.AccountListFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:layout="@layout/account_list"
|
||||
app:layout_scrollFlags="scroll"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/fab_margin"
|
||||
android:contentDescription="@string/login_create_account"
|
||||
app:srcCompat="@drawable/ic_add_white"/>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -1,24 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
android:id="@+id/drawer_layout"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:openDrawer="start">
|
||||
|
||||
<include
|
||||
android:id="@+id/content"
|
||||
layout="@layout/accounts_content"/>
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/nav_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:fitsSystemWindows="true"
|
||||
app:headerLayout="@layout/nav_header_accounts"
|
||||
app:menu="@menu/activity_accounts_drawer"/>
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
|
@ -16,8 +16,9 @@
|
|||
<string name="help">Help</string>
|
||||
<string name="manage_accounts">Manage accounts</string>
|
||||
<string name="navigate_up">Navigate up</string>
|
||||
<string name="no_internet_sync_scheduled">No Internet, scheduling sync</string>
|
||||
<string name="share">Share</string>
|
||||
<string name="no_internet_sync_scheduled">No internet, scheduling sync</string>
|
||||
<string name="sync_started">Synchronization started</string>
|
||||
|
||||
<string name="database_destructive_migration_title">Database corrupted</string>
|
||||
<string name="database_destructive_migration_text">All accounts have been removed locally.</string>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
buildscript {
|
||||
ext.versions = [
|
||||
aboutLibraries: '10.9.1',
|
||||
accompanist: '0.30.1',
|
||||
appIntro: '7.0.0-beta02',
|
||||
composeBom: '2023.10.01',
|
||||
hilt: '2.48.1',
|
||||
|
|
Loading…
Reference in a new issue