mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-23 19:50:18 +00:00
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:
parent
36e377001f
commit
c62874a34b
|
@ -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
|
||||
)
|
|
@ -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() {
|
|||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
1
app/src/main/res/drawable/ic_folder_refresh_outline.xml
Normal file
1
app/src/main/res/drawable/ic_folder_refresh_outline.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue