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

Commit c62874a3 authored by Arnau Mora's avatar Arnau Mora Committed by Ricki Hirner
Browse files

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



* Added refresh collections fab

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

* Added listener for clicks on refresh collections fab

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

* Adjusted sizing

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

* Updated tooltip and description for collections sync

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

* Removed Snackbar

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

* Added warning for null serviceId

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

* Changed refresh collections service id fetching method

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

* Tooltip updates on refresh collections list

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

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

* Added refresh collections fab

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

* Added listener for clicks on refresh collections fab

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

* Adjusted sizing

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

* Updated tooltip and description for collections sync

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

* Removed Snackbar

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

* Added warning for null serviceId

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

* Changed refresh collections service id fetching method

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

* Tooltip updates on refresh collections list

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

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

* Changed collections refresh action update method

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

* Use lambda syntax for observers

---------

Signed-off-by: default avatarArnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: default avatarArnau Mora <arnyminerz@proton.me>
Co-authored-by: default avatarRicki Hirner <hirner@bitfire.at>
Co-authored-by: default avatarSunik Kupfer <kupfer@bitfire.at>
parent 36e37700
Loading
Loading
Loading
Loading
+10 −1
Original line number Diff line number Diff line
@@ -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?

@@ -35,3 +39,8 @@ interface ServiceDao {
    fun renameAccount(oldName: String, newName: String)

}

data class ServiceTypeAndId(
    @ColumnInfo(name = "type") val type: String,
    @ColumnInfo(name = "id") val id: Long
)
 No newline at end of file
+97 −76
Original line number Diff line number Diff line
@@ -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
        })

        // "Sync now" button
        model.networkAvailable.observe(this, Observer { networkAvailable ->
        }

        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

            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() {
    }


    // adapter
    // 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)

    class TabsAdapter(
            val activity: AppCompatActivity
    ): FragmentStatePagerAdapter(activity.supportFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
            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)
        }

        var cardDavSvcId: Long? = null
            set(value) {
                field = value
                recalculate()
        binding.refresh.setOnClickListener {
            fragment.onRefresh()
        }
        var calDavSvcId: Long? = null
            set(value) {
                field = value
                recalculate()
    }

        private var idxCardDav: Int? = null
        private var idxCalDav: Int? = null
        private var idxWebcal: Int? = null

        private fun recalculate() {

    // adapter

    class FragmentsAdapter(
        val activity: FragmentActivity,
        private val cardDavSvcId: Long?,
        private val calDavSvcId: Long?
    ): FragmentStateAdapter(activity) {

        private val idxCardDav: Int?
        private val idxCalDav: Int?
        private val idxWebcal: Int?

        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() =
        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
                idxCardDav ->
                    AddressBooksFragment().apply {
                        arguments = Bundle(2).apply {
                            putLong(CollectionsFragment.EXTRA_SERVICE_ID, cardDavSvcId!!)
                            putString(CollectionsFragment.EXTRA_COLLECTION_TYPE, Collection.TYPE_ADDRESSBOOK)
                        }
                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
                idxCalDav ->
                    CalendarsFragment().apply {
                        arguments = Bundle(2).apply {
                            putLong(CollectionsFragment.EXTRA_SERVICE_ID, calDavSvcId!!)
                            putString(CollectionsFragment.EXTRA_COLLECTION_TYPE, Collection.TYPE_CALENDAR)
                        }
                    }
            throw IllegalArgumentException()
                idxWebcal ->
                    WebcalFragment().apply {
                        arguments = Bundle(2).apply {
                            putLong(CollectionsFragment.EXTRA_SERVICE_ID, calDavSvcId!!)
                            putString(CollectionsFragment.EXTRA_COLLECTION_TYPE, Collection.TYPE_WEBCAL)
                        }
                    }
                else -> 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>()
+11 −10
Original line number Diff line number Diff line
@@ -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 −0
Original line number Diff line number Diff line
<!-- 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>
 No newline at end of file
+15 −1
Original line number Diff line number Diff line
@@ -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>
 No newline at end of file
Loading