1
0
Fork 0

For #2165 - Implement pull-to-refresh gesture to sync history.

master
person808 2020-06-08 14:58:49 -07:00 committed by Kainalu Hagiwara
parent f163861b47
commit d14b39a56e
9 changed files with 128 additions and 4 deletions

View File

@ -10,6 +10,8 @@ import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.res.Resources import android.content.res.Resources
import androidx.navigation.NavController import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.engine.prompt.ShareData
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
@ -25,6 +27,7 @@ interface HistoryController {
fun handleDeleteSome(items: Set<HistoryItem>) fun handleDeleteSome(items: Set<HistoryItem>)
fun handleCopyUrl(item: HistoryItem) fun handleCopyUrl(item: HistoryItem)
fun handleShare(item: HistoryItem) fun handleShare(item: HistoryItem)
fun handleRequestSync()
} }
class DefaultHistoryController( class DefaultHistoryController(
@ -33,16 +36,21 @@ class DefaultHistoryController(
private val resources: Resources, private val resources: Resources,
private val snackbar: FenixSnackbar, private val snackbar: FenixSnackbar,
private val clipboardManager: ClipboardManager, private val clipboardManager: ClipboardManager,
private val scope: CoroutineScope,
private val openToBrowser: (item: HistoryItem, mode: BrowsingMode?) -> Unit, private val openToBrowser: (item: HistoryItem, mode: BrowsingMode?) -> Unit,
private val displayDeleteAll: () -> Unit, private val displayDeleteAll: () -> Unit,
private val invalidateOptionsMenu: () -> Unit, private val invalidateOptionsMenu: () -> Unit,
private val deleteHistoryItems: (Set<HistoryItem>) -> Unit private val deleteHistoryItems: (Set<HistoryItem>) -> Unit,
private val syncHistory: suspend () -> Unit
) : HistoryController { ) : HistoryController {
override fun handleOpen(item: HistoryItem, mode: BrowsingMode?) { override fun handleOpen(item: HistoryItem, mode: BrowsingMode?) {
openToBrowser(item, mode) openToBrowser(item, mode)
} }
override fun handleSelect(item: HistoryItem) { override fun handleSelect(item: HistoryItem) {
if (store.state.mode === HistoryFragmentState.Mode.Syncing) {
return
}
store.dispatch(HistoryFragmentAction.AddItemForRemoval(item)) store.dispatch(HistoryFragmentAction.AddItemForRemoval(item))
} }
@ -87,4 +95,12 @@ class DefaultHistoryController(
) )
) )
} }
override fun handleRequestSync() {
scope.launch {
store.dispatch(HistoryFragmentAction.StartSync)
syncHistory.invoke()
store.dispatch(HistoryFragmentAction.FinishSync)
}
}
} }

View File

@ -24,6 +24,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.service.fxa.sync.SyncReason
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
@ -72,10 +73,12 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
isDisplayedWithBrowserToolbar = false isDisplayedWithBrowserToolbar = false
), ),
activity?.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager, activity?.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager,
lifecycleScope,
::openItem, ::openItem,
::displayDeleteAllDialog, ::displayDeleteAllDialog,
::invalidateOptionsMenu, ::invalidateOptionsMenu,
::deleteHistoryItems ::deleteHistoryItems,
::syncHistory
) )
historyInteractor = HistoryInteractor( historyInteractor = HistoryInteractor(
historyController historyController
@ -268,4 +271,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
) )
nav(R.id.historyFragment, directions) nav(R.id.historyFragment, directions)
} }
private suspend fun syncHistory() {
val accountManager = requireComponents.backgroundServices.accountManager
accountManager.syncNowAsync(SyncReason.User).await()
viewModel.invalidate()
}
} }

View File

@ -32,6 +32,8 @@ sealed class HistoryFragmentAction : Action {
data class RemoveItemForRemoval(val item: HistoryItem) : HistoryFragmentAction() data class RemoveItemForRemoval(val item: HistoryItem) : HistoryFragmentAction()
object EnterDeletionMode : HistoryFragmentAction() object EnterDeletionMode : HistoryFragmentAction()
object ExitDeletionMode : HistoryFragmentAction() object ExitDeletionMode : HistoryFragmentAction()
object StartSync : HistoryFragmentAction()
object FinishSync : HistoryFragmentAction()
} }
/** /**
@ -45,6 +47,7 @@ data class HistoryFragmentState(val items: List<HistoryItem>, val mode: Mode) :
object Normal : Mode() object Normal : Mode()
object Deleting : Mode() object Deleting : Mode()
object Syncing : Mode()
data class Editing(override val selectedItems: Set<HistoryItem>) : Mode() data class Editing(override val selectedItems: Set<HistoryItem>) : Mode()
} }
} }
@ -72,5 +75,7 @@ private fun historyStateReducer(
is HistoryFragmentAction.ExitEditMode -> state.copy(mode = HistoryFragmentState.Mode.Normal) is HistoryFragmentAction.ExitEditMode -> state.copy(mode = HistoryFragmentState.Mode.Normal)
is HistoryFragmentAction.EnterDeletionMode -> state.copy(mode = HistoryFragmentState.Mode.Deleting) is HistoryFragmentAction.EnterDeletionMode -> state.copy(mode = HistoryFragmentState.Mode.Deleting)
is HistoryFragmentAction.ExitDeletionMode -> state.copy(mode = HistoryFragmentState.Mode.Normal) 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)
} }
} }

View File

@ -57,4 +57,8 @@ class HistoryInteractor(
override fun onDeleteSome(items: Set<HistoryItem>) { override fun onDeleteSome(items: Set<HistoryItem>) {
historyController.handleDeleteSome(items) historyController.handleDeleteSome(items)
} }
override fun onRequestSync() {
historyController.handleRequestSync()
}
} }

View File

@ -16,6 +16,7 @@ import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibraryPageView import org.mozilla.fenix.library.LibraryPageView
import org.mozilla.fenix.library.SelectionInteractor import org.mozilla.fenix.library.SelectionInteractor
import org.mozilla.fenix.theme.ThemeManager
/** /**
* Interface for the HistoryViewInteractor. This interface is implemented by objects that want * Interface for the HistoryViewInteractor. This interface is implemented by objects that want
@ -71,6 +72,11 @@ interface HistoryViewInteractor : SelectionInteractor<HistoryItem> {
* @param items the history items to delete * @param items the history items to delete
*/ */
fun onDeleteSome(items: Set<HistoryItem>) fun onDeleteSome(items: Set<HistoryItem>)
/**
* Called when the user requests a sync of the history
*/
fun onRequestSync()
} }
/** /**
@ -97,12 +103,23 @@ class HistoryView(
adapter = historyAdapter adapter = historyAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (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) { fun update(state: HistoryFragmentState) {
val oldMode = mode val oldMode = mode
view.progress_bar.isVisible = state.mode === HistoryFragmentState.Mode.Deleting 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 items = state.items
mode = state.mode mode = state.mode

View File

@ -33,9 +33,16 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="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:id="@+id/history_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:listitem="@layout/history_list_item"/> tools:listitem="@layout/history_list_item"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -9,10 +9,14 @@ import android.content.ClipboardManager
import android.content.res.Resources import android.content.res.Resources
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import io.mockk.coVerifyOrder
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.slot import io.mockk.slot
import io.mockk.verify 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 mozilla.components.concept.engine.prompt.ShareData
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
@ -26,9 +30,11 @@ import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
// Robolectric needed for `onShareItem()` // Robolectric needed for `onShareItem()`
@ExperimentalCoroutinesApi
@RunWith(FenixRobolectricTestRunner::class) @RunWith(FenixRobolectricTestRunner::class)
class HistoryControllerTest { class HistoryControllerTest {
private val historyItem = HistoryItem(0, "title", "url", 0.toLong()) private val historyItem = HistoryItem(0, "title", "url", 0.toLong())
private val scope: CoroutineScope = TestCoroutineScope()
private val store: HistoryFragmentStore = mockk(relaxed = true) private val store: HistoryFragmentStore = mockk(relaxed = true)
private val state: HistoryFragmentState = mockk(relaxed = true) private val state: HistoryFragmentState = mockk(relaxed = true)
private val navController: NavController = mockk(relaxed = true) private val navController: NavController = mockk(relaxed = true)
@ -39,16 +45,19 @@ class HistoryControllerTest {
private val displayDeleteAll: () -> Unit = mockk(relaxed = true) private val displayDeleteAll: () -> Unit = mockk(relaxed = true)
private val invalidateOptionsMenu: () -> Unit = mockk(relaxed = true) private val invalidateOptionsMenu: () -> Unit = mockk(relaxed = true)
private val deleteHistoryItems: (Set<HistoryItem>) -> 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( private val controller = DefaultHistoryController(
store, store,
navController, navController,
resources, resources,
snackbar, snackbar,
clipboardManager, clipboardManager,
scope,
openInBrowser, openInBrowser,
displayDeleteAll, displayDeleteAll,
invalidateOptionsMenu, invalidateOptionsMenu,
deleteHistoryItems deleteHistoryItems,
syncHistory
) )
@Before @Before
@ -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 @Test
fun onBackPressedInNormalMode() { fun onBackPressedInNormalMode() {
every { state.mode } returns HistoryFragmentState.Mode.Normal every { state.mode } returns HistoryFragmentState.Mode.Normal
@ -190,4 +210,19 @@ class HistoryControllerTest {
assertEquals(historyItem.title, (directions.captured.arguments["data"] as Array<ShareData>)[0].title) assertEquals(historyItem.title, (directions.captured.arguments["data"] as Array<ShareData>)[0].title)
assertEquals(historyItem.url, (directions.captured.arguments["data"] as Array<ShareData>)[0].url) 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)
}
}
} }

View File

@ -46,6 +46,29 @@ class HistoryFragmentStoreTest {
assertEquals(store.state.mode, HistoryFragmentState.Mode.Editing(setOf(historyItem))) 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( private fun emptyDefaultState(): HistoryFragmentState = HistoryFragmentState(
items = listOf(), items = listOf(),
mode = HistoryFragmentState.Mode.Normal mode = HistoryFragmentState.Mode.Normal

View File

@ -120,4 +120,12 @@ class HistoryInteractorTest {
controller.handleDeleteSome(items) controller.handleDeleteSome(items)
} }
} }
@Test
fun onRequestSync() {
interactor.onRequestSync()
verifyAll {
controller.handleRequestSync()
}
}
} }