For #1048 - Add ability to view tab history by long-pressing the back or forward button.
parent
2a0a11f740
commit
921b16233b
@ -0,0 +1,21 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix
|
||||
|
||||
/**
|
||||
* Interface for features and fragments that want to handle long presses of the system back button
|
||||
*/
|
||||
interface OnBackLongPressedListener {
|
||||
|
||||
/**
|
||||
* Called when the system back button is long pressed.
|
||||
*
|
||||
* Note: This cannot be called when gesture navigation is enabled on Android 10+ due to system
|
||||
* limitations.
|
||||
*
|
||||
* @return true if the event was handled
|
||||
*/
|
||||
fun onBackLongPressed(): Boolean
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.tabhistory
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
data class TabHistoryItem(
|
||||
val title: String,
|
||||
val url: String,
|
||||
val index: Int,
|
||||
val isSelected: Boolean
|
||||
)
|
||||
|
||||
class TabHistoryAdapter(
|
||||
private val interactor: TabHistoryViewInteractor
|
||||
) : RecyclerView.Adapter<TabHistoryViewHolder>() {
|
||||
|
||||
var historyList: List<TabHistoryItem> = emptyList()
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabHistoryViewHolder {
|
||||
val view =
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.history_list_item, parent, false)
|
||||
return TabHistoryViewHolder(view, interactor)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TabHistoryViewHolder, position: Int) {
|
||||
holder.bind(historyList[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = historyList.size
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.tabhistory
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import mozilla.components.feature.session.SessionUseCases
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
interface TabHistoryController {
|
||||
fun handleGoToHistoryItem(item: TabHistoryItem)
|
||||
}
|
||||
|
||||
class DefaultTabHistoryController(
|
||||
private val navController: NavController,
|
||||
private val goToHistoryIndexUseCase: SessionUseCases.GoToHistoryIndexUseCase
|
||||
) : TabHistoryController {
|
||||
|
||||
override fun handleGoToHistoryItem(item: TabHistoryItem) {
|
||||
navController.popBackStack(R.id.browserFragment, false)
|
||||
goToHistoryIndexUseCase.invoke(item.index)
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.tabhistory
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kotlinx.android.synthetic.main.fragment_tab_history_dialog.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
|
||||
class TabHistoryDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NO_TITLE, R.style.BottomSheet)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? = inflater.inflate(R.layout.fragment_tab_history_dialog, container, false)
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val controller = DefaultTabHistoryController(
|
||||
navController = findNavController(),
|
||||
goToHistoryIndexUseCase = requireComponents.useCases.sessionUseCases.goToHistoryIndex
|
||||
)
|
||||
val tabHistoryView = TabHistoryView(
|
||||
container = tabHistoryLayout,
|
||||
expandDialog = ::expand,
|
||||
interactor = TabHistoryInteractor(controller)
|
||||
)
|
||||
|
||||
consumeFrom(requireComponents.core.store) {
|
||||
tabHistoryView.updateState(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun expand() {
|
||||
(dialog as BottomSheetDialog).behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.tabhistory
|
||||
|
||||
class TabHistoryInteractor(
|
||||
private val controller: TabHistoryController
|
||||
) : TabHistoryViewInteractor {
|
||||
|
||||
override fun goToHistoryItem(item: TabHistoryItem) {
|
||||
controller.handleGoToHistoryItem(item)
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.tabhistory
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.component_tabhistory.*
|
||||
import mozilla.components.browser.state.selector.selectedTab
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
interface TabHistoryViewInteractor {
|
||||
|
||||
/**
|
||||
* Jump to a specific index in the tab's history.
|
||||
*/
|
||||
fun goToHistoryItem(item: TabHistoryItem)
|
||||
}
|
||||
|
||||
class TabHistoryView(
|
||||
private val container: ViewGroup,
|
||||
private val expandDialog: () -> Unit,
|
||||
interactor: TabHistoryViewInteractor
|
||||
) : LayoutContainer {
|
||||
|
||||
override val containerView: View?
|
||||
get() = container
|
||||
|
||||
val view: View = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_tabhistory, container, true)
|
||||
|
||||
private val adapter = TabHistoryAdapter(interactor)
|
||||
private val layoutManager = object : LinearLayoutManager(view.context) {
|
||||
override fun onLayoutCompleted(state: RecyclerView.State?) {
|
||||
super.onLayoutCompleted(state)
|
||||
currentIndex?.let { index ->
|
||||
// Force expansion of the dialog, otherwise scrolling to the current history item
|
||||
// won't work when its position is near the bottom of the recyclerview.
|
||||
expandDialog.invoke()
|
||||
// Also, attempt to center the current history item.
|
||||
val itemView = tabHistoryRecyclerView.findViewHolderForLayoutPosition(
|
||||
findFirstCompletelyVisibleItemPosition()
|
||||
)?.itemView
|
||||
val offset = tabHistoryRecyclerView.height / 2 - (itemView?.height ?: 0) / 2
|
||||
scrollToPositionWithOffset(index, offset)
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
reverseLayout = true
|
||||
}
|
||||
|
||||
private var currentIndex: Int? = null
|
||||
|
||||
init {
|
||||
tabHistoryRecyclerView.adapter = adapter
|
||||
tabHistoryRecyclerView.layoutManager = layoutManager
|
||||
}
|
||||
|
||||
fun updateState(state: BrowserState) {
|
||||
state.selectedTab?.content?.history?.let { historyState ->
|
||||
currentIndex = historyState.currentIndex
|
||||
val items = historyState.items.mapIndexed { index, historyItem ->
|
||||
TabHistoryItem(
|
||||
title = historyItem.title,
|
||||
url = historyItem.uri,
|
||||
index = index,
|
||||
isSelected = index == historyState.currentIndex
|
||||
)
|
||||
}
|
||||
adapter.historyList = items
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.tabhistory
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.text.bold
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.history_list_item.view.*
|
||||
|
||||
class TabHistoryViewHolder(
|
||||
private val view: View,
|
||||
private val interactor: TabHistoryViewInteractor
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
fun bind(item: TabHistoryItem) {
|
||||
view.history_layout.overflowView.isVisible = false
|
||||
view.history_layout.urlView.text = item.url
|
||||
view.history_layout.loadFavicon(item.url)
|
||||
|
||||
view.history_layout.titleView.text = if (item.isSelected) {
|
||||
buildSpannedString {
|
||||
bold { append(item.title) }
|
||||
}
|
||||
} else {
|
||||
item.title
|
||||
}
|
||||
|
||||
view.setOnClickListener { interactor.goToHistoryItem(item) }
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/tabHistoryWrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
|
||||
<View
|
||||
android:id="@+id/handle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="3dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@color/secondary_text_normal_theme"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.1" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/tabHistoryRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingTop="19dp"
|
||||
tools:listitem="@layout/history_list_item" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/tabHistoryLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
@ -0,0 +1,38 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.tabhistory
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import mozilla.components.browser.session.SessionManager
|
||||
import mozilla.components.feature.session.SessionUseCases
|
||||
import org.junit.Test
|
||||
|
||||
class TabHistoryControllerTest {
|
||||
|
||||
val sessionManager: SessionManager = mockk(relaxed = true)
|
||||
val navController: NavController = mockk(relaxed = true)
|
||||
val sessionUseCases = SessionUseCases(sessionManager)
|
||||
val goToHistoryIndexUseCase = sessionUseCases.goToHistoryIndex
|
||||
val controller = DefaultTabHistoryController(
|
||||
navController = navController,
|
||||
goToHistoryIndexUseCase = goToHistoryIndexUseCase
|
||||
)
|
||||
|
||||
val currentItem = TabHistoryItem(
|
||||
index = 0,
|
||||
title = "",
|
||||
url = "",
|
||||
isSelected = true
|
||||
)
|
||||
|
||||
@Test
|
||||
fun handleGoToHistoryIndex() {
|
||||
controller.handleGoToHistoryItem(currentItem)
|
||||
|
||||
verify { goToHistoryIndexUseCase.invoke(currentItem.index) }
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.tabhistory
|
||||
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Test
|
||||
|
||||
class TabHistoryInteractorTest {
|
||||
|
||||
val controller: TabHistoryController = mockk(relaxed = true)
|
||||
val interactor = TabHistoryInteractor(controller)
|
||||
|
||||
@Test
|
||||
fun onGoToHistoryItem() {
|
||||
val item: TabHistoryItem = mockk()
|
||||
|
||||
interactor.goToHistoryItem(item)
|
||||
|
||||
verify { controller.handleGoToHistoryItem(item) }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue