Merge pull request #102 from abhijitvalluri/upstream_sync
Pull in latest mozilla commitspull/104/head
commit
4e6810bfb3
@ -0,0 +1,36 @@
|
|||||||
|
/* 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.library.recentlyclosed
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import mozilla.components.browser.state.state.ClosedTab
|
||||||
|
|
||||||
|
class RecentlyClosedAdapter(
|
||||||
|
private val interactor: RecentlyClosedFragmentInteractor
|
||||||
|
) : ListAdapter<ClosedTab, RecentlyClosedItemViewHolder>(DiffCallback) {
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): RecentlyClosedItemViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(RecentlyClosedItemViewHolder.LAYOUT_ID, parent, false)
|
||||||
|
return RecentlyClosedItemViewHolder(view, interactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecentlyClosedItemViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
private object DiffCallback : DiffUtil.ItemCallback<ClosedTab>() {
|
||||||
|
override fun areItemsTheSame(oldItem: ClosedTab, newItem: ClosedTab) =
|
||||||
|
oldItem.id == newItem.id || oldItem.title == newItem.title || oldItem.url == newItem.url
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: ClosedTab, newItem: ClosedTab) =
|
||||||
|
oldItem.id == newItem.id || oldItem.title == newItem.title || oldItem.url == newItem.url
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
/* 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.library.recentlyclosed
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.res.Resources
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import mozilla.components.browser.state.action.RecentlyClosedAction
|
||||||
|
import mozilla.components.browser.state.state.ClosedTab
|
||||||
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
|
import mozilla.components.concept.engine.prompt.ShareData
|
||||||
|
import mozilla.components.feature.recentlyclosed.ext.restoreTab
|
||||||
|
import org.mozilla.fenix.BrowserDirection
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
|
import org.mozilla.fenix.components.FenixSnackbar
|
||||||
|
|
||||||
|
interface RecentlyClosedController {
|
||||||
|
fun handleOpen(item: ClosedTab, mode: BrowsingMode? = null)
|
||||||
|
fun handleDeleteOne(tab: ClosedTab)
|
||||||
|
fun handleCopyUrl(item: ClosedTab)
|
||||||
|
fun handleShare(item: ClosedTab)
|
||||||
|
fun handleNavigateToHistory()
|
||||||
|
fun handleRestore(item: ClosedTab)
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultRecentlyClosedController(
|
||||||
|
private val navController: NavController,
|
||||||
|
private val store: BrowserStore,
|
||||||
|
private val sessionManager: SessionManager,
|
||||||
|
private val resources: Resources,
|
||||||
|
private val snackbar: FenixSnackbar,
|
||||||
|
private val clipboardManager: ClipboardManager,
|
||||||
|
private val activity: HomeActivity,
|
||||||
|
private val openToBrowser: (item: ClosedTab, mode: BrowsingMode?) -> Unit
|
||||||
|
) : RecentlyClosedController {
|
||||||
|
override fun handleOpen(item: ClosedTab, mode: BrowsingMode?) {
|
||||||
|
openToBrowser(item, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleDeleteOne(tab: ClosedTab) {
|
||||||
|
store.dispatch(RecentlyClosedAction.RemoveClosedTabAction(tab))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleNavigateToHistory() {
|
||||||
|
navController.navigate(
|
||||||
|
RecentlyClosedFragmentDirections.actionGlobalHistoryFragment(),
|
||||||
|
NavOptions.Builder().setPopUpTo(R.id.historyFragment, true).build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleCopyUrl(item: ClosedTab) {
|
||||||
|
val urlClipData = ClipData.newPlainText(item.url, item.url)
|
||||||
|
clipboardManager.setPrimaryClip(urlClipData)
|
||||||
|
with(snackbar) {
|
||||||
|
setText(resources.getString(R.string.url_copied))
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleShare(item: ClosedTab) {
|
||||||
|
navController.navigate(
|
||||||
|
RecentlyClosedFragmentDirections.actionGlobalShareFragment(
|
||||||
|
data = arrayOf(ShareData(url = item.url, title = item.title))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleRestore(item: ClosedTab) {
|
||||||
|
item.restoreTab(
|
||||||
|
store,
|
||||||
|
sessionManager,
|
||||||
|
onTabRestored = {
|
||||||
|
activity.openToBrowser(
|
||||||
|
from = BrowserDirection.FromRecentlyClosed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
/* 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.library.recentlyclosed
|
||||||
|
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import kotlinx.android.synthetic.main.fragment_recently_closed_tabs.view.*
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import mozilla.components.browser.state.state.ClosedTab
|
||||||
|
import mozilla.components.lib.state.ext.consumeFrom
|
||||||
|
import mozilla.components.lib.state.ext.flowScoped
|
||||||
|
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
|
||||||
|
import org.mozilla.fenix.BrowserDirection
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
|
import org.mozilla.fenix.components.FenixSnackbar
|
||||||
|
import org.mozilla.fenix.components.StoreProvider
|
||||||
|
import org.mozilla.fenix.ext.getRootView
|
||||||
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
|
import org.mozilla.fenix.ext.showToolbar
|
||||||
|
import org.mozilla.fenix.library.LibraryPageFragment
|
||||||
|
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
|
class RecentlyClosedFragment : LibraryPageFragment<ClosedTab>() {
|
||||||
|
private lateinit var recentlyClosedFragmentStore: RecentlyClosedFragmentStore
|
||||||
|
private var _recentlyClosedFragmentView: RecentlyClosedFragmentView? = null
|
||||||
|
protected val recentlyClosedFragmentView: RecentlyClosedFragmentView
|
||||||
|
get() = _recentlyClosedFragmentView!!
|
||||||
|
|
||||||
|
private lateinit var recentlyClosedInteractor: RecentlyClosedFragmentInteractor
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
showToolbar(getString(R.string.library_recently_closed_tabs))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.library_menu, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||||
|
R.id.close_history -> {
|
||||||
|
close()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_recently_closed_tabs, container, false)
|
||||||
|
recentlyClosedFragmentStore = StoreProvider.get(this) {
|
||||||
|
RecentlyClosedFragmentStore(
|
||||||
|
RecentlyClosedFragmentState(
|
||||||
|
items = listOf()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
recentlyClosedInteractor = RecentlyClosedFragmentInteractor(
|
||||||
|
recentlyClosedController = DefaultRecentlyClosedController(
|
||||||
|
navController = findNavController(),
|
||||||
|
store = requireComponents.core.store,
|
||||||
|
activity = activity as HomeActivity,
|
||||||
|
sessionManager = requireComponents.core.sessionManager,
|
||||||
|
resources = requireContext().resources,
|
||||||
|
snackbar = FenixSnackbar.make(
|
||||||
|
view = requireActivity().getRootView()!!,
|
||||||
|
isDisplayedWithBrowserToolbar = true
|
||||||
|
),
|
||||||
|
clipboardManager = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager,
|
||||||
|
openToBrowser = ::openItem
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_recentlyClosedFragmentView = RecentlyClosedFragmentView(
|
||||||
|
view.recentlyClosedLayout,
|
||||||
|
recentlyClosedInteractor
|
||||||
|
)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_recentlyClosedFragmentView = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openItem(tab: ClosedTab, mode: BrowsingMode? = null) {
|
||||||
|
mode?.let { (activity as HomeActivity).browsingModeManager.mode = it }
|
||||||
|
|
||||||
|
(activity as HomeActivity).openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = tab.url,
|
||||||
|
newTab = true,
|
||||||
|
from = BrowserDirection.FromRecentlyClosed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
consumeFrom(recentlyClosedFragmentStore) {
|
||||||
|
recentlyClosedFragmentView.update(it.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
requireComponents.core.store.flowScoped(viewLifecycleOwner) { flow ->
|
||||||
|
flow.map { state -> state.closedTabs }
|
||||||
|
.ifChanged()
|
||||||
|
.collect { tabs ->
|
||||||
|
recentlyClosedFragmentStore.dispatch(
|
||||||
|
RecentlyClosedFragmentAction.Change(tabs)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val selectedItems: Set<ClosedTab> = setOf()
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
/* 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.library.recentlyclosed
|
||||||
|
|
||||||
|
import mozilla.components.browser.state.state.ClosedTab
|
||||||
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interactor for the recently closed screen
|
||||||
|
* Provides implementations for the RecentlyClosedInteractor
|
||||||
|
*/
|
||||||
|
class RecentlyClosedFragmentInteractor(
|
||||||
|
private val recentlyClosedController: RecentlyClosedController
|
||||||
|
) : RecentlyClosedInteractor {
|
||||||
|
override fun restore(item: ClosedTab) {
|
||||||
|
recentlyClosedController.handleRestore(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCopyPressed(item: ClosedTab) {
|
||||||
|
recentlyClosedController.handleCopyUrl(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSharePressed(item: ClosedTab) {
|
||||||
|
recentlyClosedController.handleShare(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOpenInNormalTab(item: ClosedTab) {
|
||||||
|
recentlyClosedController.handleOpen(item, BrowsingMode.Normal)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOpenInPrivateTab(item: ClosedTab) {
|
||||||
|
recentlyClosedController.handleOpen(item, BrowsingMode.Private)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeleteOne(tab: ClosedTab) {
|
||||||
|
recentlyClosedController.handleDeleteOne(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNavigateToHistory() {
|
||||||
|
recentlyClosedController.handleNavigateToHistory()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/* 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.library.recentlyclosed
|
||||||
|
|
||||||
|
import mozilla.components.browser.state.state.ClosedTab
|
||||||
|
import mozilla.components.lib.state.Action
|
||||||
|
import mozilla.components.lib.state.State
|
||||||
|
import mozilla.components.lib.state.Store
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [Store] for holding the [RecentlyClosedFragmentState] and applying [RecentlyClosedFragmentAction]s.
|
||||||
|
*/
|
||||||
|
class RecentlyClosedFragmentStore(initialState: RecentlyClosedFragmentState) :
|
||||||
|
Store<RecentlyClosedFragmentState, RecentlyClosedFragmentAction>(
|
||||||
|
initialState,
|
||||||
|
::recentlyClosedStateReducer
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions to dispatch through the `RecentlyClosedFragmentStore` to modify
|
||||||
|
* `RecentlyClosedFragmentState` through the reducer.
|
||||||
|
*/
|
||||||
|
sealed class RecentlyClosedFragmentAction : Action {
|
||||||
|
data class Change(val list: List<ClosedTab>) : RecentlyClosedFragmentAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state for the Recently Closed Screen
|
||||||
|
* @property items List of recently closed tabs to display
|
||||||
|
*/
|
||||||
|
data class RecentlyClosedFragmentState(val items: List<ClosedTab> = emptyList()) : State
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The RecentlyClosedFragmentState Reducer.
|
||||||
|
*/
|
||||||
|
private fun recentlyClosedStateReducer(
|
||||||
|
state: RecentlyClosedFragmentState,
|
||||||
|
action: RecentlyClosedFragmentAction
|
||||||
|
): RecentlyClosedFragmentState {
|
||||||
|
return when (action) {
|
||||||
|
is RecentlyClosedFragmentAction.Change -> state.copy(items = action.list)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
/* 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.library.recentlyclosed
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
|
import kotlinx.android.synthetic.main.component_recently_closed.*
|
||||||
|
import mozilla.components.browser.state.state.ClosedTab
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
|
||||||
|
interface RecentlyClosedInteractor {
|
||||||
|
/**
|
||||||
|
* Called when an item is tapped to restore it.
|
||||||
|
*
|
||||||
|
* @param item the tapped item to restore.
|
||||||
|
*/
|
||||||
|
fun restore(item: ClosedTab)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the view more history option is tapped.
|
||||||
|
*/
|
||||||
|
fun onNavigateToHistory()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the URL of a recently closed tab item to the copy-paste buffer.
|
||||||
|
*
|
||||||
|
* @param item the recently closed tab item to copy the URL from
|
||||||
|
*/
|
||||||
|
fun onCopyPressed(item: ClosedTab)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the share sheet for a recently closed tab item.
|
||||||
|
*
|
||||||
|
* @param item the recently closed tab item to share
|
||||||
|
*/
|
||||||
|
fun onSharePressed(item: ClosedTab)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a recently closed tab item in a new tab.
|
||||||
|
*
|
||||||
|
* @param item the recently closed tab item to open in a new tab
|
||||||
|
*/
|
||||||
|
fun onOpenInNormalTab(item: ClosedTab)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a recently closed tab item in a private tab.
|
||||||
|
*
|
||||||
|
* @param item the recently closed tab item to open in a private tab
|
||||||
|
*/
|
||||||
|
fun onOpenInPrivateTab(item: ClosedTab)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes one recently closed tab item.
|
||||||
|
*
|
||||||
|
* @param item the recently closed tab item to delete.
|
||||||
|
*/
|
||||||
|
fun onDeleteOne(tab: ClosedTab)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View that contains and configures the Recently Closed List
|
||||||
|
*/
|
||||||
|
class RecentlyClosedFragmentView(
|
||||||
|
container: ViewGroup,
|
||||||
|
private val interactor: RecentlyClosedFragmentInteractor
|
||||||
|
) : LayoutContainer {
|
||||||
|
|
||||||
|
override val containerView: ConstraintLayout = LayoutInflater.from(container.context)
|
||||||
|
.inflate(R.layout.component_recently_closed, container, true)
|
||||||
|
.findViewById(R.id.recently_closed_wrapper)
|
||||||
|
|
||||||
|
private val recentlyClosedAdapter: RecentlyClosedAdapter = RecentlyClosedAdapter(interactor)
|
||||||
|
|
||||||
|
init {
|
||||||
|
recently_closed_list.apply {
|
||||||
|
layoutManager = LinearLayoutManager(containerView.context)
|
||||||
|
adapter = recentlyClosedAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
view_more_history.apply {
|
||||||
|
titleView.text =
|
||||||
|
containerView.context.getString(R.string.recently_closed_show_full_history)
|
||||||
|
urlView.isVisible = false
|
||||||
|
overflowView.isVisible = false
|
||||||
|
iconView.background = null
|
||||||
|
iconView.setImageDrawable(
|
||||||
|
ContextCompat.getDrawable(
|
||||||
|
containerView.context,
|
||||||
|
R.drawable.ic_history
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setOnClickListener {
|
||||||
|
interactor.onNavigateToHistory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(items: List<ClosedTab>) {
|
||||||
|
recently_closed_empty_view.isVisible = items.isEmpty()
|
||||||
|
recently_closed_list.isVisible = items.isNotEmpty()
|
||||||
|
recentlyClosedAdapter.submitList(items)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
/* 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.library.recentlyclosed
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.android.synthetic.main.history_list_item.view.*
|
||||||
|
import mozilla.components.browser.state.state.ClosedTab
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.library.history.HistoryItemMenu
|
||||||
|
import org.mozilla.fenix.utils.Do
|
||||||
|
|
||||||
|
class RecentlyClosedItemViewHolder(
|
||||||
|
view: View,
|
||||||
|
private val recentlyClosedFragmentInteractor: RecentlyClosedFragmentInteractor
|
||||||
|
) : RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
|
private var item: ClosedTab? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
setupMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(
|
||||||
|
item: ClosedTab
|
||||||
|
) {
|
||||||
|
itemView.history_layout.titleView.text =
|
||||||
|
if (item.title.isNotEmpty()) item.title else item.url
|
||||||
|
itemView.history_layout.urlView.text = item.url
|
||||||
|
|
||||||
|
if (this.item?.url != item.url) {
|
||||||
|
itemView.history_layout.loadFavicon(item.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
recentlyClosedFragmentInteractor.restore(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.item = item
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupMenu() {
|
||||||
|
val historyMenu = HistoryItemMenu(itemView.context) {
|
||||||
|
val item = this.item ?: return@HistoryItemMenu
|
||||||
|
Do exhaustive when (it) {
|
||||||
|
HistoryItemMenu.Item.Copy -> recentlyClosedFragmentInteractor.onCopyPressed(item)
|
||||||
|
HistoryItemMenu.Item.Share -> recentlyClosedFragmentInteractor.onSharePressed(item)
|
||||||
|
HistoryItemMenu.Item.OpenInNewTab -> recentlyClosedFragmentInteractor.onOpenInNormalTab(
|
||||||
|
item
|
||||||
|
)
|
||||||
|
HistoryItemMenu.Item.OpenInPrivateTab -> recentlyClosedFragmentInteractor.onOpenInPrivateTab(
|
||||||
|
item
|
||||||
|
)
|
||||||
|
HistoryItemMenu.Item.Delete -> recentlyClosedFragmentInteractor.onDeleteOne(
|
||||||
|
item
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemView.history_layout.attachMenu(historyMenu.menuController)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LAYOUT_ID = R.layout.history_list_item
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
<?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/. -->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M20.207 18.793L15.914 14.5l3.043-3.043a1 1 0 0 0 0-1.414A5.234 5.234 0 0 0 15.232 8.5h-0.214a3.269 3.269 0 0 1-3.268-3.268V4.5a1 1 0 0 0-1.707-0.707l-6.25 6.25A1 1 0 0 0 4.5 11.75h0.732A3.269 3.269 0 0 1 8.5 15.018v0.211A4.8 4.8 0 0 0 10.087 19a1 1 0 0 0 1.37-0.041l3.043-3.045 4.293 4.293a1 1 0 0 0 1.414-1.414z"
|
||||||
|
android:fillColor="?mozac_widget_favicon_border_color"/>
|
||||||
|
</vector>
|
@ -0,0 +1,38 @@
|
|||||||
|
<?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/recently_closed_wrapper"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<org.mozilla.fenix.library.LibrarySiteItemView
|
||||||
|
android:id="@+id/view_more_history"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recently_closed_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/view_more_history"
|
||||||
|
tools:listitem="@layout/history_list_item" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/recently_closed_empty_view"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="@string/recently_closed_empty_message"
|
||||||
|
android:textColor="?secondaryText"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,9 @@
|
|||||||
|
<?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/. -->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/recentlyClosedLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical" />
|
@ -0,0 +1,171 @@
|
|||||||
|
/* 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.library.recentlyclosed
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.res.Resources
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import io.mockk.Runs
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.slot
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||||
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import mozilla.components.browser.state.action.RecentlyClosedAction
|
||||||
|
import mozilla.components.browser.state.state.ClosedTab
|
||||||
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
|
import mozilla.components.concept.engine.prompt.ShareData
|
||||||
|
import mozilla.components.feature.recentlyclosed.ext.restoreTab
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
|
import org.mozilla.fenix.components.FenixSnackbar
|
||||||
|
import org.mozilla.fenix.ext.directionsEq
|
||||||
|
import org.mozilla.fenix.ext.optionsEq
|
||||||
|
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||||
|
|
||||||
|
// Robolectric needed for `onShareItem()`
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
@RunWith(FenixRobolectricTestRunner::class)
|
||||||
|
class DefaultRecentlyClosedControllerTest {
|
||||||
|
private val dispatcher = TestCoroutineDispatcher()
|
||||||
|
private val navController: NavController = mockk(relaxed = true)
|
||||||
|
private val resources: Resources = mockk(relaxed = true)
|
||||||
|
private val snackbar: FenixSnackbar = mockk(relaxed = true)
|
||||||
|
private val clipboardManager: ClipboardManager = mockk(relaxed = true)
|
||||||
|
private val openToBrowser: (ClosedTab, BrowsingMode?) -> Unit = mockk(relaxed = true)
|
||||||
|
private val sessionManager: SessionManager = mockk(relaxed = true)
|
||||||
|
private val activity: HomeActivity = mockk(relaxed = true)
|
||||||
|
private val store: BrowserStore = mockk(relaxed = true)
|
||||||
|
val mockedTab: ClosedTab = mockk(relaxed = true)
|
||||||
|
|
||||||
|
private val controller = DefaultRecentlyClosedController(
|
||||||
|
navController,
|
||||||
|
store,
|
||||||
|
sessionManager,
|
||||||
|
resources,
|
||||||
|
snackbar,
|
||||||
|
clipboardManager,
|
||||||
|
activity,
|
||||||
|
openToBrowser
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
mockkStatic("mozilla.components.feature.recentlyclosed.ext.ClosedTabKt")
|
||||||
|
every { mockedTab.restoreTab(any(), any(), any()) } just Runs
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
dispatcher.cleanupTestCoroutines()
|
||||||
|
unmockkStatic("mozilla.components.feature.recentlyclosed.ext.ClosedTabKt")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleOpen() {
|
||||||
|
val item: ClosedTab = mockk(relaxed = true)
|
||||||
|
|
||||||
|
controller.handleOpen(item, BrowsingMode.Private)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
openToBrowser(item, BrowsingMode.Private)
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.handleOpen(item, BrowsingMode.Normal)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
openToBrowser(item, BrowsingMode.Normal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleDeleteOne() {
|
||||||
|
val item: ClosedTab = mockk(relaxed = true)
|
||||||
|
|
||||||
|
controller.handleDeleteOne(item)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
store.dispatch(RecentlyClosedAction.RemoveClosedTabAction(item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleNavigateToHistory() {
|
||||||
|
controller.handleNavigateToHistory()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
navController.navigate(
|
||||||
|
directionsEq(
|
||||||
|
RecentlyClosedFragmentDirections.actionGlobalHistoryFragment()
|
||||||
|
),
|
||||||
|
optionsEq(NavOptions.Builder().setPopUpTo(R.id.historyFragment, true).build())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleCopyUrl() {
|
||||||
|
val item = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
|
||||||
|
|
||||||
|
val clipdata = slot<ClipData>()
|
||||||
|
|
||||||
|
controller.handleCopyUrl(item)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
clipboardManager.setPrimaryClip(capture(clipdata))
|
||||||
|
snackbar.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(1, clipdata.captured.itemCount)
|
||||||
|
assertEquals("mozilla.org", clipdata.captured.description.label)
|
||||||
|
assertEquals("mozilla.org", clipdata.captured.getItemAt(0).text)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun handleShare() {
|
||||||
|
val item = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
|
||||||
|
|
||||||
|
controller.handleShare(item)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
navController.navigate(
|
||||||
|
directionsEq(
|
||||||
|
RecentlyClosedFragmentDirections.actionGlobalShareFragment(
|
||||||
|
data = arrayOf(ShareData(url = item.url, title = item.title))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleRestore() {
|
||||||
|
controller.handleRestore(mockedTab)
|
||||||
|
|
||||||
|
dispatcher.advanceUntilIdle()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
mockedTab.restoreTab(
|
||||||
|
store,
|
||||||
|
sessionManager,
|
||||||
|
onTabRestored = any()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
/* 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.library.recentlyclosed
|
||||||
|
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import mozilla.components.browser.state.state.ClosedTab
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
|
|
||||||
|
class RecentlyClosedFragmentInteractorTest {
|
||||||
|
|
||||||
|
lateinit var interactor: RecentlyClosedFragmentInteractor
|
||||||
|
private val defaultRecentlyClosedController: DefaultRecentlyClosedController =
|
||||||
|
mockk(relaxed = true)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
interactor =
|
||||||
|
RecentlyClosedFragmentInteractor(
|
||||||
|
recentlyClosedController = defaultRecentlyClosedController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun open() {
|
||||||
|
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
|
||||||
|
interactor.restore(tab)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
defaultRecentlyClosedController.handleRestore(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onCopyPressed() {
|
||||||
|
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
|
||||||
|
interactor.onCopyPressed(tab)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
defaultRecentlyClosedController.handleCopyUrl(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onSharePressed() {
|
||||||
|
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
|
||||||
|
interactor.onSharePressed(tab)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
defaultRecentlyClosedController.handleShare(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onOpenInNormalTab() {
|
||||||
|
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
|
||||||
|
interactor.onOpenInNormalTab(tab)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
defaultRecentlyClosedController.handleOpen(tab, mode = BrowsingMode.Normal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onOpenInPrivateTab() {
|
||||||
|
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
|
||||||
|
interactor.onOpenInPrivateTab(tab)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
defaultRecentlyClosedController.handleOpen(tab, mode = BrowsingMode.Private)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onDeleteOne() {
|
||||||
|
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
|
||||||
|
interactor.onDeleteOne(tab)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
defaultRecentlyClosedController.handleDeleteOne(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onNavigateToHistory() {
|
||||||
|
interactor.onNavigateToHistory()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
defaultRecentlyClosedController.handleNavigateToHistory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,89 +0,0 @@
|
|||||||
/* 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.wifi
|
|
||||||
|
|
||||||
import io.mockk.Called
|
|
||||||
import io.mockk.Runs
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.just
|
|
||||||
import io.mockk.mockk
|
|
||||||
import io.mockk.verify
|
|
||||||
import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY_AUDIBLE
|
|
||||||
import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY_INAUDIBLE
|
|
||||||
import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_ALLOW_ALL
|
|
||||||
import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_ALLOW_ON_WIFI
|
|
||||||
import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_ALL
|
|
||||||
import org.mozilla.fenix.utils.Settings
|
|
||||||
|
|
||||||
class SitePermissionsWifiIntegrationTest {
|
|
||||||
|
|
||||||
private lateinit var settings: Settings
|
|
||||||
private lateinit var wifiConnectionMonitor: WifiConnectionMonitor
|
|
||||||
private lateinit var wifiIntegration: SitePermissionsWifiIntegration
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
settings = mockk()
|
|
||||||
wifiConnectionMonitor = mockk(relaxed = true)
|
|
||||||
wifiIntegration = SitePermissionsWifiIntegration(settings, wifiConnectionMonitor)
|
|
||||||
|
|
||||||
every { settings.setSitePermissionsPhoneFeatureAction(any(), any()) } just Runs
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `add and remove wifi connected listener`() {
|
|
||||||
wifiIntegration.addWifiConnectedListener()
|
|
||||||
verify { wifiConnectionMonitor.register(any()) }
|
|
||||||
|
|
||||||
wifiIntegration.removeWifiConnectedListener()
|
|
||||||
verify { wifiConnectionMonitor.unregister(any()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `start and stop wifi connection monitor`() {
|
|
||||||
wifiIntegration.start()
|
|
||||||
verify { wifiConnectionMonitor.start() }
|
|
||||||
|
|
||||||
wifiIntegration.stop()
|
|
||||||
verify { wifiConnectionMonitor.stop() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `add only if autoplay is only allowed on wifi`() {
|
|
||||||
every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ALL
|
|
||||||
wifiIntegration.maybeAddWifiConnectedListener()
|
|
||||||
verify { wifiConnectionMonitor wasNot Called }
|
|
||||||
|
|
||||||
every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ON_WIFI
|
|
||||||
wifiIntegration.maybeAddWifiConnectedListener()
|
|
||||||
verify { wifiConnectionMonitor.register(any()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `listener removes itself if autoplay is not only allowed on wifi`() {
|
|
||||||
every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ALL
|
|
||||||
wifiIntegration.onWifiConnectionChanged(connected = true)
|
|
||||||
verify { wifiConnectionMonitor.unregister(any()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `listener sets audible and inaudible settings to allowed on connect`() {
|
|
||||||
every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ON_WIFI
|
|
||||||
wifiIntegration.onWifiConnectionChanged(connected = true)
|
|
||||||
verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_AUDIBLE, Action.ALLOWED) }
|
|
||||||
verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_INAUDIBLE, Action.ALLOWED) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `listener sets audible and inaudible settings to blocked on disconnected`() {
|
|
||||||
every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ON_WIFI
|
|
||||||
wifiIntegration.onWifiConnectionChanged(connected = false)
|
|
||||||
verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_AUDIBLE, Action.BLOCKED) }
|
|
||||||
verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_INAUDIBLE, Action.BLOCKED) }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
/* 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.wifi
|
|
||||||
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.NetworkRequest
|
|
||||||
import io.mockk.Runs
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.just
|
|
||||||
import io.mockk.mockk
|
|
||||||
import io.mockk.mockkConstructor
|
|
||||||
import io.mockk.slot
|
|
||||||
import io.mockk.unmockkConstructor
|
|
||||||
import io.mockk.verify
|
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class WifiConnectionMonitorTest {
|
|
||||||
|
|
||||||
private lateinit var connectivityManager: ConnectivityManager
|
|
||||||
private lateinit var wifiConnectionMonitor: WifiConnectionMonitor
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
mockkConstructor(NetworkRequest.Builder::class)
|
|
||||||
connectivityManager = mockk(relaxUnitFun = true)
|
|
||||||
wifiConnectionMonitor = WifiConnectionMonitor(connectivityManager)
|
|
||||||
|
|
||||||
every {
|
|
||||||
anyConstructed<NetworkRequest.Builder>().addTransportType(any())
|
|
||||||
} answers { self as NetworkRequest.Builder }
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun teardown() {
|
|
||||||
unmockkConstructor(NetworkRequest.Builder::class)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `start runs only once`() {
|
|
||||||
wifiConnectionMonitor.start()
|
|
||||||
wifiConnectionMonitor.start()
|
|
||||||
|
|
||||||
verify(exactly = 1) {
|
|
||||||
connectivityManager.registerNetworkCallback(any(), any<ConnectivityManager.NetworkCallback>())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `stop only runs after start`() {
|
|
||||||
wifiConnectionMonitor.stop()
|
|
||||||
verify(exactly = 0) {
|
|
||||||
connectivityManager.unregisterNetworkCallback(any<ConnectivityManager.NetworkCallback>())
|
|
||||||
}
|
|
||||||
|
|
||||||
wifiConnectionMonitor.start()
|
|
||||||
wifiConnectionMonitor.stop()
|
|
||||||
verify {
|
|
||||||
connectivityManager.unregisterNetworkCallback(any<ConnectivityManager.NetworkCallback>())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `passes results from connectivity manager to observers`() {
|
|
||||||
val slot = slot<ConnectivityManager.NetworkCallback>()
|
|
||||||
every { connectivityManager.registerNetworkCallback(any(), capture(slot)) } just Runs
|
|
||||||
|
|
||||||
wifiConnectionMonitor.start()
|
|
||||||
|
|
||||||
// Immediately notifies observer when registered
|
|
||||||
val observer = mockk<WifiConnectionMonitor.Observer>(relaxed = true)
|
|
||||||
wifiConnectionMonitor.register(observer)
|
|
||||||
verify { observer.onWifiConnectionChanged(connected = false) }
|
|
||||||
|
|
||||||
// Notifies observer when network is available or lost
|
|
||||||
slot.captured.onAvailable(mockk())
|
|
||||||
verify { observer.onWifiConnectionChanged(connected = true) }
|
|
||||||
|
|
||||||
slot.captured.onLost(mockk())
|
|
||||||
verify { observer.onWifiConnectionChanged(connected = false) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun captureNetworkCallback(): ConnectivityManager.NetworkCallback {
|
|
||||||
val slot = slot<ConnectivityManager.NetworkCallback>()
|
|
||||||
verify { connectivityManager.registerNetworkCallback(any(), capture(slot)) }
|
|
||||||
return slot.captured
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Purpose: uplift (via cherry-picking) any missing commits from an l10n bot
|
||||||
|
# from 'MAIN_BRANCH' to a specified release branch.
|
||||||
|
#
|
||||||
|
# Usage examples: (append --verbose to print out detailed information)
|
||||||
|
# Dry-run (says what will happen, doesn't do any work): ./l10n-uplift.py releases/48.0
|
||||||
|
# Uplift, actually perform the work: ./l10n-uplift.py releases/48.0 --uplift
|
||||||
|
# Process multiple branches at once: ./l10n-uplift.py releases/48.0 releases/44.0 --uplift --verbose
|
||||||
|
|
||||||
|
# Note: there can often be conflicts between cherry-picks, to catch duplication errors, build after conflict resolution: ./gradlew assembleDebug
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
# TODO don't forget to change this once we switch to 'main' or whatever other name.
|
||||||
|
MAIN_BRANCH="master"
|
||||||
|
L10N_AUTHOR="release+l10n-automation-bot@mozilla.com"
|
||||||
|
|
||||||
|
def run_cmd_checked(*args, **kwargs):
|
||||||
|
"""Run a command, throwing an exception if it exits with non-zero status."""
|
||||||
|
kwargs["check"] = True
|
||||||
|
kwargs["capture_output"] = True
|
||||||
|
# beware! only run this script with inputs from a trusted, non-external source
|
||||||
|
kwargs["shell"] = True
|
||||||
|
try:
|
||||||
|
return subprocess.run(*args, **kwargs).stdout.decode()
|
||||||
|
except subprocess.CalledProcessError as err:
|
||||||
|
print(err.stderr)
|
||||||
|
raise err
|
||||||
|
|
||||||
|
def uplift_commits(branch, verbose, uplift):
|
||||||
|
print(f"\nProcessing l10n commits for '{branch}'...")
|
||||||
|
# if necessary, this will setup 'branch' to track its upstream equivalent
|
||||||
|
run_cmd_checked([f"git checkout {branch}"])
|
||||||
|
# get l10n commits which happened on MAIN_BRANCH since 'branch' split off
|
||||||
|
commits_since_split = run_cmd_checked([f"git rev-list {branch}..{MAIN_BRANCH} --author={L10N_AUTHOR}"]).split()
|
||||||
|
# order commits by oldest-first, e.g. how we'd cherry pick them
|
||||||
|
commits_since_split.reverse()
|
||||||
|
print(f"Since '{branch}' split off '{MAIN_BRANCH}', there were {len(commits_since_split)} commit(s) from {L10N_AUTHOR}.")
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f"\nHashes of those commits on '{MAIN_BRANCH}' are: {commits_since_split}\n")
|
||||||
|
|
||||||
|
# look for 'cherry picked' commits, and get the original commit hash from the commit message (as left by 'cherry-pick -x')
|
||||||
|
commits_already_uplifted = run_cmd_checked([f"git rev-list {MAIN_BRANCH}..{branch} --author={L10N_AUTHOR} --grep=\"cherry picked\" --pretty=%b | grep cherry | cut -d' ' -f5 | cut -c 1-40"]).split()
|
||||||
|
commits_already_uplifted.reverse()
|
||||||
|
|
||||||
|
print(f"Of those, {len(commits_already_uplifted)} commit(s) already uplifted.")
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f"Hashes of commits already uplifted to '{branch}': {commits_already_uplifted}\n")
|
||||||
|
|
||||||
|
commits_to_uplift = [commit for commit in commits_since_split if commit not in commits_already_uplifted]
|
||||||
|
|
||||||
|
print(f"Need to uplift {len(commits_to_uplift)} commit(s).")
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f"Hashes of commits to uplift from '{MAIN_BRANCH}' to '{branch}': {commits_to_uplift}\n")
|
||||||
|
|
||||||
|
if len(commits_to_uplift) == 0:
|
||||||
|
print("Nothing to uplift.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if uplift:
|
||||||
|
print(f"Uplifting (for real)...")
|
||||||
|
else:
|
||||||
|
print(f"Uplifting (dry-run)...")
|
||||||
|
|
||||||
|
run_cmd_checked([f"git checkout {branch}"])
|
||||||
|
for commit in commits_to_uplift:
|
||||||
|
if verbose:
|
||||||
|
print(f"Cherry picking {commit} from '{MAIN_BRANCH}' to '{branch}'")
|
||||||
|
if uplift:
|
||||||
|
run_cmd_checked([f"git cherry-pick {commit} -x"])
|
||||||
|
if uplift:
|
||||||
|
print(f"Uplifted {len(commits_to_uplift)} commits from '{MAIN_BRANCH}' to '{branch}'")
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description=f"Uplift l10n commits from {MAIN_BRANCH} to specified branches")
|
||||||
|
parser.add_argument(
|
||||||
|
'branches', nargs='+', type=str,
|
||||||
|
help='target branches, e.g. specific release branches')
|
||||||
|
parser.add_argument(
|
||||||
|
'--verbose', default=False, action='store_true',
|
||||||
|
help='print out commit hashes and other detailed information'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--uplift', default=False, action='store_true',
|
||||||
|
help='uplift l10n commits missing from specified branches (if not specified, dry-run is performed)'
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# remember the current branch, so that we can return to it once we're done.
|
||||||
|
current_branch = run_cmd_checked(["git rev-parse --abbrev-ref HEAD"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
for branch in args.branches:
|
||||||
|
uplift_commits(branch, args.verbose, args.uplift)
|
||||||
|
finally:
|
||||||
|
# go back to the branch we were on before 'uplift_for_branches' ran
|
||||||
|
run_cmd_checked([f"git checkout {current_branch}"])
|
Loading…
Reference in New Issue