For #1048 - Add ability to view tab history by long-pressing the back or forward button.
parent
2a0a11f740
commit
921b16233b
|
@ -6,11 +6,14 @@ package org.mozilla.fenix
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewConfiguration
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
|
@ -30,6 +33,8 @@ import kotlinx.android.synthetic.main.activity_home.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import mozilla.components.browser.search.SearchEngine
|
import mozilla.components.browser.search.SearchEngine
|
||||||
import mozilla.components.browser.session.SessionManager
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
@ -139,6 +144,9 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See onKeyDown for why this is necessary
|
||||||
|
private var backLongPressJob: Job? = null
|
||||||
|
|
||||||
private lateinit var navigationToolbar: Toolbar
|
private lateinit var navigationToolbar: Toolbar
|
||||||
|
|
||||||
final override fun onCreate(savedInstanceState: Bundle?) {
|
final override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -349,6 +357,50 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isAndroidN(): Boolean =
|
||||||
|
Build.VERSION.SDK_INT == Build.VERSION_CODES.N || Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1
|
||||||
|
|
||||||
|
private fun handleBackLongPress(): Boolean {
|
||||||
|
supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
|
||||||
|
if (it is OnBackLongPressedListener && it.onBackLongPressed()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
// Inspired by https://searchfox.org/mozilla-esr68/source/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java#584-613
|
||||||
|
// Android N has broken passing onKeyLongPress events for the back button, so we
|
||||||
|
// instead implement the long press behavior ourselves
|
||||||
|
// - For short presses, we cancel the callback in onKeyUp
|
||||||
|
// - For long presses, the normal keypress is marked as cancelled, hence won't be handled elsewhere
|
||||||
|
// (but Android still provides the haptic feedback), and the long press action is run
|
||||||
|
if (isAndroidN() && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
backLongPressJob = lifecycleScope.launch {
|
||||||
|
delay(ViewConfiguration.getLongPressTimeout().toLong())
|
||||||
|
handleBackLongPress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onKeyDown(keyCode, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
if (isAndroidN() && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
backLongPressJob?.cancel()
|
||||||
|
}
|
||||||
|
return super.onKeyUp(keyCode, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
// onKeyLongPress is broken in Android N so we don't handle back button long presses here
|
||||||
|
// for N. The version check ensures we don't handle back button long presses twice.
|
||||||
|
if (!isAndroidN() && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
return handleBackLongPress()
|
||||||
|
}
|
||||||
|
return super.onKeyLongPress(keyCode, event)
|
||||||
|
}
|
||||||
|
|
||||||
final override fun onUserLeaveHint() {
|
final override fun onUserLeaveHint() {
|
||||||
supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
|
supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
|
||||||
if (it is UserInteractionHandler && it.onHomePressed()) {
|
if (it is UserInteractionHandler && it.onHomePressed()) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -77,6 +77,7 @@ import org.mozilla.fenix.FeatureFlags
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.IntentReceiverActivity
|
import org.mozilla.fenix.IntentReceiverActivity
|
||||||
import org.mozilla.fenix.NavGraphDirections
|
import org.mozilla.fenix.NavGraphDirections
|
||||||
|
import org.mozilla.fenix.OnBackLongPressedListener
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
|
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
|
||||||
|
@ -116,7 +117,8 @@ import java.lang.ref.WeakReference
|
||||||
*/
|
*/
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
@Suppress("TooManyFunctions", "LargeClass")
|
@Suppress("TooManyFunctions", "LargeClass")
|
||||||
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, SessionManager.Observer {
|
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, SessionManager.Observer,
|
||||||
|
OnBackLongPressedListener {
|
||||||
private lateinit var browserFragmentStore: BrowserFragmentStore
|
private lateinit var browserFragmentStore: BrowserFragmentStore
|
||||||
private lateinit var browserAnimator: BrowserAnimator
|
private lateinit var browserAnimator: BrowserAnimator
|
||||||
|
|
||||||
|
@ -757,6 +759,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
||||||
removeSessionIfNeeded()
|
removeSessionIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBackLongPressed(): Boolean {
|
||||||
|
findNavController().navigate(R.id.action_global_tabHistoryDialogFragment)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the external app session ID to be restored later in [onViewStateRestored].
|
* Saves the external app session ID to be restored later in [onViewStateRestored].
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -170,7 +170,13 @@ class DefaultBrowserToolbarController(
|
||||||
|
|
||||||
Do exhaustive when (item) {
|
Do exhaustive when (item) {
|
||||||
ToolbarMenu.Item.Back -> sessionUseCases.goBack.invoke(currentSession)
|
ToolbarMenu.Item.Back -> sessionUseCases.goBack.invoke(currentSession)
|
||||||
ToolbarMenu.Item.Forward -> sessionUseCases.goForward.invoke(currentSession)
|
is ToolbarMenu.Item.Forward -> {
|
||||||
|
if (item.viewHistory) {
|
||||||
|
navController.navigate(R.id.action_global_tabHistoryDialogFragment)
|
||||||
|
} else {
|
||||||
|
sessionUseCases.goForward.invoke(currentSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
ToolbarMenu.Item.Reload -> sessionUseCases.reload.invoke(currentSession)
|
ToolbarMenu.Item.Reload -> sessionUseCases.reload.invoke(currentSession)
|
||||||
ToolbarMenu.Item.Stop -> sessionUseCases.stopLoading.invoke(currentSession)
|
ToolbarMenu.Item.Stop -> sessionUseCases.stopLoading.invoke(currentSession)
|
||||||
ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically {
|
ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically {
|
||||||
|
@ -333,7 +339,7 @@ class DefaultBrowserToolbarController(
|
||||||
private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) {
|
private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) {
|
||||||
val eventItem = when (item) {
|
val eventItem = when (item) {
|
||||||
ToolbarMenu.Item.Back -> Event.BrowserMenuItemTapped.Item.BACK
|
ToolbarMenu.Item.Back -> Event.BrowserMenuItemTapped.Item.BACK
|
||||||
ToolbarMenu.Item.Forward -> Event.BrowserMenuItemTapped.Item.FORWARD
|
is ToolbarMenu.Item.Forward -> Event.BrowserMenuItemTapped.Item.FORWARD
|
||||||
ToolbarMenu.Item.Reload -> Event.BrowserMenuItemTapped.Item.RELOAD
|
ToolbarMenu.Item.Reload -> Event.BrowserMenuItemTapped.Item.RELOAD
|
||||||
ToolbarMenu.Item.Stop -> Event.BrowserMenuItemTapped.Item.STOP
|
ToolbarMenu.Item.Stop -> Event.BrowserMenuItemTapped.Item.STOP
|
||||||
ToolbarMenu.Item.Settings -> Event.BrowserMenuItemTapped.Item.SETTINGS
|
ToolbarMenu.Item.Settings -> Event.BrowserMenuItemTapped.Item.SETTINGS
|
||||||
|
|
|
@ -82,9 +82,10 @@ class DefaultToolbarMenu(
|
||||||
session?.canGoForward ?: true
|
session?.canGoForward ?: true
|
||||||
},
|
},
|
||||||
secondaryImageTintResource = ThemeManager.resolveAttribute(R.attr.disabled, context),
|
secondaryImageTintResource = ThemeManager.resolveAttribute(R.attr.disabled, context),
|
||||||
disableInSecondaryState = true
|
disableInSecondaryState = true,
|
||||||
|
longClickListener = { onItemTapped.invoke(ToolbarMenu.Item.Forward(viewHistory = true)) }
|
||||||
) {
|
) {
|
||||||
onItemTapped.invoke(ToolbarMenu.Item.Forward)
|
onItemTapped.invoke(ToolbarMenu.Item.Forward(viewHistory = false))
|
||||||
}
|
}
|
||||||
|
|
||||||
val refresh = BrowserMenuItemToolbar.TwoStateButton(
|
val refresh = BrowserMenuItemToolbar.TwoStateButton(
|
||||||
|
|
|
@ -14,7 +14,7 @@ interface ToolbarMenu {
|
||||||
object FindInPage : Item()
|
object FindInPage : Item()
|
||||||
object Share : Item()
|
object Share : Item()
|
||||||
object Back : Item()
|
object Back : Item()
|
||||||
object Forward : Item()
|
data class Forward(val viewHistory: Boolean) : Item()
|
||||||
object Reload : Item()
|
object Reload : Item()
|
||||||
object Stop : Item()
|
object Stop : Item()
|
||||||
object OpenInFenix : Item()
|
object OpenInFenix : Item()
|
||||||
|
|
|
@ -75,9 +75,10 @@ class CustomTabToolbarMenu(
|
||||||
R.attr.disabled,
|
R.attr.disabled,
|
||||||
context
|
context
|
||||||
),
|
),
|
||||||
disableInSecondaryState = true
|
disableInSecondaryState = true,
|
||||||
|
longClickListener = { onItemTapped.invoke(ToolbarMenu.Item.Forward(viewHistory = true)) }
|
||||||
) {
|
) {
|
||||||
onItemTapped.invoke(ToolbarMenu.Item.Forward)
|
onItemTapped.invoke(ToolbarMenu.Item.Forward(viewHistory = false))
|
||||||
}
|
}
|
||||||
|
|
||||||
val refresh = BrowserMenuItemToolbar.TwoStateButton(
|
val refresh = BrowserMenuItemToolbar.TwoStateButton(
|
||||||
|
|
|
@ -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" />
|
|
@ -102,6 +102,9 @@
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_global_savedLoginsAuthFragment"
|
android:id="@+id/action_global_savedLoginsAuthFragment"
|
||||||
app:destination="@id/savedLoginsAuthFragment" />
|
app:destination="@id/savedLoginsAuthFragment" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_global_tabHistoryDialogFragment"
|
||||||
|
app:destination="@id/tabHistoryDialogFragment" />
|
||||||
|
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/tabTrayDialogFragment"
|
android:id="@+id/tabTrayDialogFragment"
|
||||||
|
@ -859,4 +862,7 @@
|
||||||
app:argType="string"
|
app:argType="string"
|
||||||
app:nullable="true" />
|
app:nullable="true" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
<dialog
|
||||||
|
android:id="@+id/tabHistoryDialogFragment"
|
||||||
|
android:name="org.mozilla.fenix.tabhistory.TabHistoryDialogFragment" />
|
||||||
</navigation>
|
</navigation>
|
||||||
|
|
|
@ -220,7 +220,7 @@ class DefaultBrowserToolbarControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun handleToolbarForwardPress() = runBlockingTest {
|
fun handleToolbarForwardPress() = runBlockingTest {
|
||||||
val item = ToolbarMenu.Item.Forward
|
val item = ToolbarMenu.Item.Forward(false)
|
||||||
|
|
||||||
val controller = createController(scope = this)
|
val controller = createController(scope = this)
|
||||||
controller.handleToolbarItemInteraction(item)
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
@ -229,6 +229,17 @@ class DefaultBrowserToolbarControllerTest {
|
||||||
verify { sessionUseCases.goForward(currentSession) }
|
verify { sessionUseCases.goForward(currentSession) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarForwardLongPress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.Forward(true)
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.FORWARD)) }
|
||||||
|
verify { navController.navigate(R.id.action_global_tabHistoryDialogFragment) }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun handleToolbarReloadPress() = runBlockingTest {
|
fun handleToolbarReloadPress() = runBlockingTest {
|
||||||
val item = ToolbarMenu.Item.Reload
|
val item = ToolbarMenu.Item.Reload
|
||||||
|
|
|
@ -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