Make "Refresh collection list" more visible (bitfireAT/davx5#266)

* Added refresh collections fab

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Added listener for clicks on refresh collections fab

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Adjusted sizing

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Updated tooltip and description for collections sync

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Removed Snackbar

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added warning for null serviceId

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Changed refresh collections service id fetching method

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Tooltip updates on refresh collections list

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Migrate to ViewPager2; show "Refresh collections" for WebCal, too

* Added refresh collections fab

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Added listener for clicks on refresh collections fab

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Adjusted sizing

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Updated tooltip and description for collections sync

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Removed Snackbar

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added warning for null serviceId

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Changed refresh collections service id fetching method

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Tooltip updates on refresh collections list

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Migrate to ViewPager2; show "Refresh collections" for WebCal, too

* Changed collections refresh action update method

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Use lambda syntax for observers

---------

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
Co-authored-by: Sunik Kupfer <kupfer@bitfire.at>
This commit is contained in:
Arnau Mora 2023-07-12 12:10:09 +02:00 committed by Ricki Hirner
parent 36e377001f
commit c62874a34b
6 changed files with 133 additions and 86 deletions

View file

@ -5,6 +5,7 @@
package at.bitfire.davdroid.db
import androidx.lifecycle.LiveData
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
@ -19,6 +20,9 @@ interface ServiceDao {
@Query("SELECT id FROM service WHERE accountName=:accountName AND type=:type")
fun getIdByAccountAndType(accountName: String, type: String): LiveData<Long>
@Query("SELECT type, id FROM service WHERE accountName=:accountName")
fun getServiceTypeAndIdsByAccount(accountName: String): LiveData<List<ServiceTypeAndId>>
@Query("SELECT * FROM service WHERE id=:id")
fun get(id: Long): Service?
@ -34,4 +38,9 @@ interface ServiceDao {
@Query("UPDATE service SET accountName=:newName WHERE accountName=:oldName")
fun renameAccount(oldName: String, newName: String)
}
}
data class ServiceTypeAndId(
@ColumnInfo(name = "type") val type: String,
@ColumnInfo(name = "id") val id: Long
)

View file

@ -17,9 +17,10 @@ import android.view.Menu
import android.view.MenuItem
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.appcompat.widget.TooltipCompat
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.*
import androidx.viewpager2.adapter.FragmentStateAdapter
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.ActivityAccountBinding
import at.bitfire.davdroid.db.AppDatabase
@ -31,6 +32,7 @@ import at.bitfire.davdroid.syncadapter.SyncWorker
import at.bitfire.davdroid.ui.AppWarningsManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayoutMediator
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -72,29 +74,37 @@ class AccountActivity: AppCompatActivity() {
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
model.accountExists.observe(this, Observer { accountExists ->
model.accountExists.observe(this) { accountExists ->
if (!accountExists)
finish()
})
}
binding.tabLayout.setupWithViewPager(binding.viewPager)
val tabsAdapter = TabsAdapter(this)
binding.viewPager.adapter = tabsAdapter
model.cardDavService.observe(this, Observer {
tabsAdapter.cardDavSvcId = it
})
model.calDavService.observe(this, Observer {
tabsAdapter.calDavSvcId = it
})
model.services.observe(this) { services ->
val calDavServiceId = services.firstOrNull { it.type == Service.TYPE_CALDAV }?.id
val cardDavServiceId = services.firstOrNull { it.type == Service.TYPE_CARDDAV }?.id
// "Sync now" button
model.networkAvailable.observe(this, Observer { networkAvailable ->
val viewPager = binding.viewPager
val adapter = FragmentsAdapter(this, cardDavServiceId, calDavServiceId)
viewPager.adapter = adapter
// connect ViewPager with TabLayout (top bar with tabs)
TabLayoutMediator(binding.tabLayout, viewPager) { tab, position ->
tab.text = adapter.getHeading(position)
}.attach()
}
// "Sync now" fab
model.networkAvailable.observe(this) { networkAvailable ->
binding.sync.setOnClickListener {
if (!networkAvailable)
Snackbar.make(binding.sync, R.string.no_internet_sync_scheduled, Snackbar.LENGTH_LONG).show()
Snackbar.make(
binding.sync,
R.string.no_internet_sync_scheduled,
Snackbar.LENGTH_LONG
).show()
SyncWorker.enqueueAllAuthorities(this, model.account)
}
})
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -155,28 +165,48 @@ class AccountActivity: AppCompatActivity() {
}
// public functions
/**
* Updates the click listener of the refresh collections list FAB, according to the given
* fragment. Should be called when the related fragment is resumed.
*/
fun updateRefreshCollectionsListAction(fragment: CollectionsFragment) {
val label = when (fragment) {
is AddressBooksFragment ->
getString(R.string.account_refresh_address_book_list)
is CalendarsFragment,
is WebcalFragment ->
getString(R.string.account_refresh_calendar_list)
else -> null
}
if (label != null) {
binding.refresh.contentDescription = label
TooltipCompat.setTooltipText(binding.refresh, label)
}
binding.refresh.setOnClickListener {
fragment.onRefresh()
}
}
// adapter
class TabsAdapter(
val activity: AppCompatActivity
): FragmentStatePagerAdapter(activity.supportFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
class FragmentsAdapter(
val activity: FragmentActivity,
private val cardDavSvcId: Long?,
private val calDavSvcId: Long?
): FragmentStateAdapter(activity) {
var cardDavSvcId: Long? = null
set(value) {
field = value
recalculate()
}
var calDavSvcId: Long? = null
set(value) {
field = value
recalculate()
}
private val idxCardDav: Int?
private val idxCalDav: Int?
private val idxWebcal: Int?
private var idxCardDav: Int? = null
private var idxCalDav: Int? = null
private var idxWebcal: Int? = null
private fun recalculate() {
init {
var currentIndex = 0
idxCardDav = if (cardDavSvcId != null)
@ -191,48 +221,40 @@ class AccountActivity: AppCompatActivity() {
idxCalDav = null
idxWebcal = null
}
// reflect changes in UI
notifyDataSetChanged()
}
override fun getCount() =
(if (idxCardDav != null) 1 else 0) +
(if (idxCalDav != null) 1 else 0) +
(if (idxWebcal != null) 1 else 0)
override fun getItemCount() =
(if (idxCardDav != null) 1 else 0) +
(if (idxCalDav != null) 1 else 0) +
(if (idxWebcal != null) 1 else 0)
override fun getItem(position: Int): Fragment {
val args = Bundle(1)
override fun createFragment(position: Int) =
when (position) {
idxCardDav -> {
val frag = AddressBooksFragment()
args.putLong(CollectionsFragment.EXTRA_SERVICE_ID, cardDavSvcId!!)
args.putString(CollectionsFragment.EXTRA_COLLECTION_TYPE, Collection.TYPE_ADDRESSBOOK)
frag.arguments = args
return frag
}
idxCalDav -> {
val frag = CalendarsFragment()
args.putLong(CollectionsFragment.EXTRA_SERVICE_ID, calDavSvcId!!)
args.putString(CollectionsFragment.EXTRA_COLLECTION_TYPE, Collection.TYPE_CALENDAR)
frag.arguments = args
return frag
}
idxWebcal -> {
val frag = WebcalFragment()
args.putLong(CollectionsFragment.EXTRA_SERVICE_ID, calDavSvcId!!)
args.putString(CollectionsFragment.EXTRA_COLLECTION_TYPE, Collection.TYPE_WEBCAL)
frag.arguments = args
return frag
}
idxCardDav ->
AddressBooksFragment().apply {
arguments = Bundle(2).apply {
putLong(CollectionsFragment.EXTRA_SERVICE_ID, cardDavSvcId!!)
putString(CollectionsFragment.EXTRA_COLLECTION_TYPE, Collection.TYPE_ADDRESSBOOK)
}
}
idxCalDav ->
CalendarsFragment().apply {
arguments = Bundle(2).apply {
putLong(CollectionsFragment.EXTRA_SERVICE_ID, calDavSvcId!!)
putString(CollectionsFragment.EXTRA_COLLECTION_TYPE, Collection.TYPE_CALENDAR)
}
}
idxWebcal ->
WebcalFragment().apply {
arguments = Bundle(2).apply {
putLong(CollectionsFragment.EXTRA_SERVICE_ID, calDavSvcId!!)
putString(CollectionsFragment.EXTRA_COLLECTION_TYPE, Collection.TYPE_WEBCAL)
}
}
else -> throw IllegalArgumentException()
}
throw IllegalArgumentException()
}
// required to reload all fragments
override fun getItemPosition(obj: Any) = POSITION_NONE
override fun getPageTitle(position: Int): String =
fun getHeading(position: Int) =
when (position) {
idxCardDav -> activity.getString(R.string.account_carddav)
idxCalDav -> activity.getString(R.string.account_caldav)
@ -261,8 +283,7 @@ class AccountActivity: AppCompatActivity() {
val accountSettings by lazy { AccountSettings(application, account) }
val accountExists = MutableLiveData<Boolean>()
val cardDavService = db.serviceDao().getIdByAccountAndType(account.name, Service.TYPE_CARDDAV)
val calDavService = db.serviceDao().getIdByAccountAndType(account.name, Service.TYPE_CALDAV)
val services = db.serviceDao().getServiceTypeAndIdsByAccount(account.name)
val showOnlyPersonal = MutableLiveData<Boolean>()
val showOnlyPersonalWritable = MutableLiveData<Boolean>()
@ -312,4 +333,4 @@ class AccountActivity: AppCompatActivity() {
}
}
}

View file

@ -4,6 +4,7 @@
package at.bitfire.davdroid.ui.account
import android.app.Application
import android.content.*
import android.os.Bundle
import android.provider.CalendarContract
@ -35,7 +36,6 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -52,7 +52,7 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
val accountModel by activityViewModels<AccountActivity.Model>()
@Inject lateinit var modelFactory: Model.Factory
val model by viewModels<Model> {
protected val model by viewModels<Model> {
object: ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T: ViewModel> create(modelClass: Class<T>): T =
@ -170,6 +170,7 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
override fun onResume() {
super.onResume()
checkPermissions()
(activity as? AccountActivity)?.updateRefreshCollectionsListAction(this)
}
override fun onDestroyView() {
@ -262,12 +263,12 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
class Model @AssistedInject constructor(
@ApplicationContext val context: Context,
application: Application,
val db: AppDatabase,
@Assisted val accountModel: AccountActivity.Model,
@Assisted val serviceId: Long,
@Assisted val collectionType: String
): ViewModel() {
): AndroidViewModel(application) {
@AssistedFactory
interface Factory {
@ -275,7 +276,7 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
}
// cache task provider
val taskProvider by lazy { TaskUtils.currentProvider(context) }
val taskProvider by lazy { TaskUtils.currentProvider(getApplication()) }
val hasWriteableCollections = db.homeSetDao().hasBindableByServiceLive(serviceId)
val collectionColors = db.collectionDao().colorsByServiceLive(serviceId)
@ -299,19 +300,19 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
}
// observe RefreshCollectionsWorker status
val isRefreshing = RefreshCollectionsWorker.isWorkerInState(context, RefreshCollectionsWorker.workerName(serviceId), WorkInfo.State.RUNNING)
val isRefreshing = RefreshCollectionsWorker.isWorkerInState(getApplication(), RefreshCollectionsWorker.workerName(serviceId), WorkInfo.State.RUNNING)
// observe SyncWorker state
private val authorities =
if (collectionType == Collection.TYPE_ADDRESSBOOK)
listOf(context.getString(R.string.address_books_authority), ContactsContract.AUTHORITY)
listOf(getApplication<Application>().getString(R.string.address_books_authority), ContactsContract.AUTHORITY)
else
listOf(CalendarContract.AUTHORITY, taskProvider?.authority).filterNotNull()
val isSyncActive = SyncWorker.exists(context,
val isSyncActive = SyncWorker.exists(getApplication(),
listOf(WorkInfo.State.RUNNING),
accountModel.account,
authorities)
val isSyncPending = SyncWorker.exists(context,
val isSyncPending = SyncWorker.exists(getApplication(),
listOf(WorkInfo.State.ENQUEUED),
accountModel.account,
authorities)
@ -319,7 +320,7 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
// actions
fun refresh() {
RefreshCollectionsWorker.refreshCollections(context, serviceId)
RefreshCollectionsWorker.refreshCollections(getApplication(), serviceId)
}
}

View file

@ -0,0 +1 @@
<!-- drawable/folder_refresh_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#000000" android:pathData="M18 14.5C19.11 14.5 20.11 14.95 20.83 15.67L22 14.5V18.5H18L19.77 16.73C19.32 16.28 18.69 16 18 16C16.62 16 15.5 17.12 15.5 18.5C15.5 19.88 16.62 21 18 21C18.82 21 19.54 20.61 20 20H21.71C21.12 21.47 19.68 22.5 18 22.5C15.79 22.5 14 20.71 14 18.5C14 16.29 15.79 14.5 18 14.5M20 8H4V18H12L12 18.5C12 19 12.06 19.5 12.17 20H4C2.89 20 2 19.1 2 18L2 6C2 4.89 2.89 4 4 4H10L12 6H20C21.1 6 22 6.89 22 8V13C21.39 12.63 20.72 12.34 20 12.17V8Z" /></vector>

View file

@ -30,7 +30,7 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -38,6 +38,18 @@
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
style="@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon"
android:id="@+id/refresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_margin="@dimen/fab_margin"
android:contentDescription="@string/account_synchronize_collections"
app:srcCompat="@drawable/ic_folder_refresh_outline"
app:layout_anchor="@id/sync"
app:layout_anchorGravity="top|center"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
style="@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon"
android:id="@+id/sync"
@ -45,7 +57,9 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:contentDescription="@string/account_synchronize_now"
android:tooltipText="@string/account_synchronize_now"
app:useCompatPadding="true"
app:srcCompat="@drawable/ic_sync" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -239,6 +239,7 @@
<string name="account_no_webcals">There are no calendar subscriptions (yet).</string>
<string name="account_swipe_down">Swipe down to refresh the list from the server.</string>
<string name="account_synchronize_now">Synchronize now</string>
<string name="account_synchronize_collections">Synchronize collections</string>
<string name="account_settings">Account settings</string>
<string name="account_rename">Rename account</string>
<string name="account_rename_new_name">Unsaved local data may be dismissed. Re-synchronization is required after renaming. New account name:</string>