Skip to content

Commit

Permalink
For mozilla-mobile#2165 - Implement pull-to-refresh gesture to sync h…
Browse files Browse the repository at this point in the history
…istory.
  • Loading branch information
person808 committed Jun 11, 2020
1 parent f163861 commit 3283a03
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.res.Resources
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.concept.engine.prompt.ShareData
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
Expand All @@ -25,6 +27,7 @@ interface HistoryController {
fun handleDeleteSome(items: Set<HistoryItem>)
fun handleCopyUrl(item: HistoryItem)
fun handleShare(item: HistoryItem)
fun handleRequestSync()
}

class DefaultHistoryController(
Expand All @@ -33,16 +36,21 @@ class DefaultHistoryController(
private val resources: Resources,
private val snackbar: FenixSnackbar,
private val clipboardManager: ClipboardManager,
private val scope: CoroutineScope,
private val openToBrowser: (item: HistoryItem, mode: BrowsingMode?) -> Unit,
private val displayDeleteAll: () -> Unit,
private val invalidateOptionsMenu: () -> Unit,
private val deleteHistoryItems: (Set<HistoryItem>) -> Unit
private val deleteHistoryItems: (Set<HistoryItem>) -> Unit,
private val syncHistory: suspend () -> Unit
) : HistoryController {
override fun handleOpen(item: HistoryItem, mode: BrowsingMode?) {
openToBrowser(item, mode)
}

override fun handleSelect(item: HistoryItem) {
if (store.state.mode === HistoryFragmentState.Mode.Syncing) {
return
}
store.dispatch(HistoryFragmentAction.AddItemForRemoval(item))
}

Expand Down Expand Up @@ -87,4 +95,12 @@ class DefaultHistoryController(
)
)
}

override fun handleRequestSync() {
scope.launch {
store.dispatch(HistoryFragmentAction.StartSync)
syncHistory.invoke()
store.dispatch(HistoryFragmentAction.FinishSync)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.service.fxa.sync.SyncReason
import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
Expand Down Expand Up @@ -72,10 +73,12 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
isDisplayedWithBrowserToolbar = false
),
activity?.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager,
lifecycleScope,
::openItem,
::displayDeleteAllDialog,
::invalidateOptionsMenu,
::deleteHistoryItems
::deleteHistoryItems,
::syncHistory
)
historyInteractor = HistoryInteractor(
historyController
Expand Down Expand Up @@ -268,4 +271,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
)
nav(R.id.historyFragment, directions)
}

private suspend fun syncHistory() {
val accountManager = requireComponents.backgroundServices.accountManager
accountManager.syncNowAsync(SyncReason.User).await()
viewModel.invalidate()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ sealed class HistoryFragmentAction : Action {
data class RemoveItemForRemoval(val item: HistoryItem) : HistoryFragmentAction()
object EnterDeletionMode : HistoryFragmentAction()
object ExitDeletionMode : HistoryFragmentAction()
object StartSync : HistoryFragmentAction()
object FinishSync : HistoryFragmentAction()
}

/**
Expand All @@ -45,6 +47,7 @@ data class HistoryFragmentState(val items: List<HistoryItem>, val mode: Mode) :

object Normal : Mode()
object Deleting : Mode()
object Syncing : Mode()
data class Editing(override val selectedItems: Set<HistoryItem>) : Mode()
}
}
Expand Down Expand Up @@ -72,5 +75,7 @@ private fun historyStateReducer(
is HistoryFragmentAction.ExitEditMode -> state.copy(mode = HistoryFragmentState.Mode.Normal)
is HistoryFragmentAction.EnterDeletionMode -> state.copy(mode = HistoryFragmentState.Mode.Deleting)
is HistoryFragmentAction.ExitDeletionMode -> state.copy(mode = HistoryFragmentState.Mode.Normal)
is HistoryFragmentAction.StartSync -> state.copy(mode = HistoryFragmentState.Mode.Syncing)
is HistoryFragmentAction.FinishSync -> state.copy(mode = HistoryFragmentState.Mode.Normal)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,8 @@ class HistoryInteractor(
override fun onDeleteSome(items: Set<HistoryItem>) {
historyController.handleDeleteSome(items)
}

override fun onRequestSync() {
historyController.handleRequestSync()
}
}
17 changes: 17 additions & 0 deletions app/src/main/java/org/mozilla/fenix/library/history/HistoryView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibraryPageView
import org.mozilla.fenix.library.SelectionInteractor
import org.mozilla.fenix.theme.ThemeManager

/**
* Interface for the HistoryViewInteractor. This interface is implemented by objects that want
Expand Down Expand Up @@ -71,6 +72,11 @@ interface HistoryViewInteractor : SelectionInteractor<HistoryItem> {
* @param items the history items to delete
*/
fun onDeleteSome(items: Set<HistoryItem>)

/**
* Called when the user requests a sync of the history
*/
fun onRequestSync()
}

/**
Expand All @@ -97,12 +103,23 @@ class HistoryView(
adapter = historyAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}

val primaryTextColor =
ThemeManager.resolveAttribute(R.attr.primaryText, context)
view.swipe_refresh.setColorSchemeColors(primaryTextColor)
view.swipe_refresh.setOnRefreshListener {
interactor.onRequestSync()
view.history_list.scrollToPosition(0)
}
}

fun update(state: HistoryFragmentState) {
val oldMode = mode

view.progress_bar.isVisible = state.mode === HistoryFragmentState.Mode.Deleting
view.swipe_refresh.isRefreshing = state.mode === HistoryFragmentState.Mode.Syncing
view.swipe_refresh.isEnabled =
state.mode === HistoryFragmentState.Mode.Normal || state.mode === HistoryFragmentState.Mode.Syncing
items = state.items
mode = state.mode

Expand Down
9 changes: 8 additions & 1 deletion app/src/main/res/layout/component_history.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,16 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.recyclerview.widget.RecyclerView

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/history_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/history_list_item"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ import android.content.ClipboardManager
import android.content.res.Resources
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import io.mockk.coVerifyOrder
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.concept.engine.prompt.ShareData
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
Expand All @@ -26,9 +30,11 @@ import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner

// Robolectric needed for `onShareItem()`
@ExperimentalCoroutinesApi
@RunWith(FenixRobolectricTestRunner::class)
class HistoryControllerTest {
private val historyItem = HistoryItem(0, "title", "url", 0.toLong())
private val scope: CoroutineScope = TestCoroutineScope()
private val store: HistoryFragmentStore = mockk(relaxed = true)
private val state: HistoryFragmentState = mockk(relaxed = true)
private val navController: NavController = mockk(relaxed = true)
Expand All @@ -39,16 +45,19 @@ class HistoryControllerTest {
private val displayDeleteAll: () -> Unit = mockk(relaxed = true)
private val invalidateOptionsMenu: () -> Unit = mockk(relaxed = true)
private val deleteHistoryItems: (Set<HistoryItem>) -> Unit = mockk(relaxed = true)
private val syncHistory: suspend () -> Unit = mockk(relaxed = true)
private val controller = DefaultHistoryController(
store,
navController,
resources,
snackbar,
clipboardManager,
scope,
openInBrowser,
displayDeleteAll,
invalidateOptionsMenu,
deleteHistoryItems
deleteHistoryItems,
syncHistory
)

@Before
Expand Down Expand Up @@ -105,6 +114,17 @@ class HistoryControllerTest {
}
}

@Test
fun onSelectHistoryItemDuringSync() {
every { state.mode } returns HistoryFragmentState.Mode.Syncing

controller.handleSelect(historyItem)

verify(exactly = 0) {
store.dispatch(HistoryFragmentAction.AddItemForRemoval(historyItem))
}
}

@Test
fun onBackPressedInNormalMode() {
every { state.mode } returns HistoryFragmentState.Mode.Normal
Expand Down Expand Up @@ -190,4 +210,19 @@ class HistoryControllerTest {
assertEquals(historyItem.title, (directions.captured.arguments["data"] as Array<ShareData>)[0].title)
assertEquals(historyItem.url, (directions.captured.arguments["data"] as Array<ShareData>)[0].url)
}

@Test
fun onRequestSync() {
controller.handleRequestSync()

verify(exactly = 2) {
store.dispatch(any())
}

coVerifyOrder {
store.dispatch(HistoryFragmentAction.StartSync)
syncHistory.invoke()
store.dispatch(HistoryFragmentAction.FinishSync)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,29 @@ class HistoryFragmentStoreTest {
assertEquals(store.state.mode, HistoryFragmentState.Mode.Editing(setOf(historyItem)))
}

@Test
fun startSync() = runBlocking {
val initialState = emptyDefaultState()
val store = HistoryFragmentStore(initialState)

store.dispatch(HistoryFragmentAction.StartSync).join()
assertNotSame(initialState, store.state)
assertEquals(HistoryFragmentState.Mode.Syncing, store.state.mode)
}

@Test
fun finishSync() = runBlocking {
val initialState = HistoryFragmentState(
items = listOf(),
mode = HistoryFragmentState.Mode.Syncing
)
val store = HistoryFragmentStore(initialState)

store.dispatch(HistoryFragmentAction.FinishSync).join()
assertNotSame(initialState, store.state)
assertEquals(HistoryFragmentState.Mode.Normal, store.state.mode)
}

private fun emptyDefaultState(): HistoryFragmentState = HistoryFragmentState(
items = listOf(),
mode = HistoryFragmentState.Mode.Normal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,12 @@ class HistoryInteractorTest {
controller.handleDeleteSome(items)
}
}

@Test
fun onRequestSync() {
interactor.onRequestSync()
verifyAll {
controller.handleRequestSync()
}
}
}

0 comments on commit 3283a03

Please sign in to comment.