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