1
0
Fork 0

Fixes #916, fixes #917, fixes #920: Save, share, and delete bookmarks

master
Colin Lee 2019-03-21 14:41:41 -05:00
parent d7717e295b
commit bd81e72239
35 changed files with 1143 additions and 43 deletions

View File

@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- #916 - Added the ability to save and delete bookmarks
- #356 - Adds the ability to delete history
### Changed
### Removed

View File

@ -251,6 +251,8 @@ dependencies {
implementation Deps.leanplum
implementation Deps.mozilla_places
implementation Deps.mozilla_concept_engine
implementation Deps.mozilla_concept_storage
implementation Deps.mozilla_concept_toolbar

View File

@ -28,6 +28,7 @@ import mozilla.components.support.utils.SafeIntent
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.search.SearchFragmentDirections
import org.mozilla.fenix.settings.SettingsFragmentDirections
@ -80,8 +81,8 @@ open class HomeActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
// There is no session, or it has timed out; we should pop everything to home
if (components.core.sessionStorage.current() == null) {
// There is no session, or it has timed out; we should pop everything to home if not in private mode
if (components.core.sessionStorage.current() == null && !browsingModeManager.isPrivate) {
navHost.navController.popBackStack(R.id.homeFragment, false)
}
}
@ -109,7 +110,6 @@ open class HomeActivity : AppCompatActivity() {
return
}
}
super.onBackPressed()
}
@ -166,6 +166,8 @@ open class HomeActivity : AppCompatActivity() {
BrowserDirection.FromSearch -> SearchFragmentDirections.actionSearchFragmentToBrowserFragment(sessionId)
BrowserDirection.FromSettings ->
SettingsFragmentDirections.actionSettingsFragmentToBrowserFragment(sessionId)
BrowserDirection.FromBookmarks ->
BookmarkFragmentDirections.actionBookmarkFragmentToBrowserFragment(sessionId)
}
navHost.navController.navigate(directions)
@ -202,5 +204,5 @@ open class HomeActivity : AppCompatActivity() {
}
enum class BrowserDirection {
FromGlobal, FromHome, FromSearch, FromSettings
FromGlobal, FromHome, FromSearch, FromSettings, FromBookmarks
}

View File

@ -22,6 +22,11 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.component_search.*
import kotlinx.android.synthetic.main.fragment_browser.view.*
import kotlinx.android.synthetic.main.fragment_search.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.toolbar.behavior.BrowserToolbarBottomBehavior
import mozilla.components.feature.contextmenu.ContextMenuCandidate
import mozilla.components.feature.contextmenu.ContextMenuFeature
@ -41,7 +46,6 @@ import org.mozilla.fenix.BrowsingModeManager
import org.mozilla.fenix.DefaultThemeManager
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.utils.ItsNotBrokenSnack
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FindInPageIntegration
import org.mozilla.fenix.components.metrics.Event
@ -52,12 +56,15 @@ import org.mozilla.fenix.components.toolbar.ToolbarIntegration
import org.mozilla.fenix.components.toolbar.ToolbarMenu
import org.mozilla.fenix.components.toolbar.ToolbarUIView
import org.mozilla.fenix.customtabs.CustomTabsIntegration
import org.mozilla.fenix.ext.asActivity
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.share
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable
import org.mozilla.fenix.quickactionsheet.QuickActionSheet
import org.mozilla.fenix.quickactionsheet.QuickActionAction
import org.mozilla.fenix.quickactionsheet.QuickActionComponent
import org.mozilla.fenix.utils.ItsNotBrokenSnack
import org.mozilla.fenix.utils.Settings
class BrowserFragment : Fragment(), BackHandler {
@ -95,8 +102,12 @@ class BrowserFragment : Fragment(), BackHandler {
)
toolbarComponent.uiView.view.apply {
setBackgroundColor(ContextCompat.getColor(view.context,
DefaultThemeManager.resolveAttribute(R.attr.browserToolbarBackground, context)))
setBackgroundColor(
ContextCompat.getColor(
view.context,
DefaultThemeManager.resolveAttribute(R.attr.browserToolbarBackground, context)
)
)
(layoutParams as CoordinatorLayout.LayoutParams).apply {
// Stop toolbar from collapsing if TalkBack is enabled
@ -112,6 +123,8 @@ class BrowserFragment : Fragment(), BackHandler {
}
}
QuickActionComponent(view.nestedScrollQuickAction, ActionBusFactory.get(this))
val activity = activity as HomeActivity
DefaultThemeManager.applyStatusBarTheme(activity.window, activity.themeManager, activity, false)
@ -132,10 +145,13 @@ class BrowserFragment : Fragment(), BackHandler {
ContextMenuCandidate.defaultCandidates(
requireContext(),
requireComponents.useCases.tabsUseCases,
view),
view.engineView),
view
),
view.engineView
),
owner = this,
view = view)
view = view
)
downloadsFeature.set(
feature = DownloadsFeature(
@ -146,7 +162,8 @@ class BrowserFragment : Fragment(), BackHandler {
requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS)
}),
owner = this,
view = view)
view = view
)
promptsFeature.set(
feature = PromptFeature(
@ -157,28 +174,33 @@ class BrowserFragment : Fragment(), BackHandler {
requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS)
}),
owner = this,
view = view)
view = view
)
sessionFeature.set(
feature = SessionFeature(
sessionManager,
SessionUseCases(sessionManager),
view.engineView,
sessionId),
sessionId
),
owner = this,
view = view)
view = view
)
findInPageIntegration.set(
feature = FindInPageIntegration(
requireComponents.core.sessionManager, view.findInPageView, view.engineView
),
owner = this,
view = view)
view = view
)
toolbarIntegration.set(
feature = (toolbarComponent.uiView as ToolbarUIView).toolbarIntegration,
owner = this,
view = view)
view = view
)
sitePermissionsFeature.set(
feature = SitePermissionsFeature(
@ -217,9 +239,6 @@ class BrowserFragment : Fragment(), BackHandler {
view = view
)
val actionSheet = view.findViewById<QuickActionSheet>(R.id.quick_action_sheet)
// actionSheet.afterInflate()
val actionEmitter = ActionBusFactory.get(this).getManagedEmitter(SearchAction::class.java)
sessionId?.let { id ->
customTabsIntegration.set(
@ -252,7 +271,8 @@ class BrowserFragment : Fragment(), BackHandler {
.findNavController(toolbarComponent.getView())
.navigate(
BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
requireComponents.core.sessionManager.selectedSession?.id)
requireComponents.core.sessionManager.selectedSession?.id
)
)
requireComponents.analytics.metrics.track(
@ -263,6 +283,45 @@ class BrowserFragment : Fragment(), BackHandler {
}
}
getAutoDisposeObservable<QuickActionAction>()
.subscribe {
when (it) {
is QuickActionAction.SharePressed -> {
requireComponents.core.sessionManager
.selectedSession?.url?.apply { requireContext().share(this) }
}
is QuickActionAction.DownloadsPressed -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "348")
}
is QuickActionAction.BookmarkPressed -> {
val session = requireComponents.core.sessionManager.selectedSession
CoroutineScope(IO).launch {
requireComponents.core.bookmarksStorage
.addItem(BookmarkRoot.Mobile.id, session!!.url, session.title, null)
launch(Main) {
val rootView =
context?.asActivity()?.window?.decorView?.findViewById<View>(android.R.id.content)
rootView?.let { view ->
Snackbar.make(
view,
getString(R.string.bookmark_created_snackbar),
Snackbar.LENGTH_LONG
)
.setAction(getString(R.string.edit_bookmark_snackbar_action)) {
ItsNotBrokenSnack(
context!!
).showSnackbar(issueNumber = "90")
}
.show()
}
}
}
}
is QuickActionAction.ReadPressed -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "908")
}
}
}
assignSitePermissionsRules()
}

View File

@ -16,6 +16,7 @@ import mozilla.components.browser.engine.gecko.GeckoEngine
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
import mozilla.components.concept.engine.DefaultSettings
import mozilla.components.concept.engine.Engine
@ -123,6 +124,9 @@ class Core(private val context: Context) {
*/
val historyStorage by lazy { PlacesHistoryStorage(context) }
val bookmarksStorage
by lazy { PlacesBookmarksStorage(context) }
/**
* Constructs a [TrackingProtectionPolicy] based on current preferences.
*

View File

@ -4,7 +4,7 @@
package org.mozilla.fenix.components.metrics
import android.content.Context
import mozilla.components.service.glean.EventMetricType
import mozilla.components.service.glean.metrics.EventMetricType
import mozilla.components.service.glean.Glean
import mozilla.components.support.utils.Browsers
import org.mozilla.fenix.BuildConfig

View File

@ -15,8 +15,10 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.fragment_library.*
import org.mozilla.fenix.utils.ItsNotBrokenSnack
import mozilla.appservices.places.BookmarkRoot
import org.mozilla.fenix.R
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentArgs
import org.mozilla.fenix.utils.ItsNotBrokenSnack
class LibraryFragment : Fragment() {
@ -47,15 +49,18 @@ class LibraryFragment : Fragment() {
null
)
)
libraryBookmarks.setOnClickListener(Navigation.createNavigateOnClickListener(
LibraryFragmentDirections.actionLibraryFragmentToBookmarksFragment(BookmarkRoot.Root.id).actionId,
BookmarkFragmentArgs(BookmarkRoot.Root.id).toBundle()
))
libraryDownloads.setOnClickListener {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "348")
}
libraryScreenshots.setOnClickListener {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "89")
}
libraryBookmarks.setOnClickListener {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "90")
}
libraryReadingList.setOnClickListener {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "913")
}

View File

@ -0,0 +1,246 @@
/* 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.bookmarks
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.bookmark_row.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.icons.IconRequest
import mozilla.components.browser.menu.BrowserMenu
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.mozilla.fenix.R
import kotlin.coroutines.CoroutineContext
class BookmarkAdapter(val actionEmitter: Observer<BookmarkAction>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var tree: List<BookmarkNode> = listOf()
private var mode: BookmarkState.Mode = BookmarkState.Mode.Normal
lateinit var job: Job
fun updateData(tree: BookmarkNode?, mode: BookmarkState.Mode) {
this.tree = tree?.children?.filterNotNull() ?: listOf()
this.mode = mode
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.bookmark_row, parent, false)
return when (viewType) {
BookmarkItemViewHolder.viewType.ordinal -> BookmarkAdapter.BookmarkItemViewHolder(
view, actionEmitter, job
)
BookmarkFolderViewHolder.viewType.ordinal -> BookmarkAdapter.BookmarkFolderViewHolder(
view, actionEmitter
)
BookmarkSeparatorViewHolder.viewType.ordinal -> BookmarkAdapter.BookmarkSeparatorViewHolder(
view, actionEmitter
)
else -> throw IllegalStateException("ViewType $viewType does not match to a ViewHolder")
}
}
override fun getItemViewType(position: Int): Int {
return when (tree[position].type) {
BookmarkNodeType.ITEM -> ViewType.ITEM.ordinal
BookmarkNodeType.FOLDER -> ViewType.FOLDER.ordinal
BookmarkNodeType.SEPARATOR -> ViewType.SEPARATOR.ordinal
else -> throw IllegalStateException("Item $tree[position] does not match to a ViewType")
}
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
job = Job()
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
job.cancel()
}
override fun getItemCount(): Int = tree.size
@SuppressWarnings("ComplexMethod")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val bookmarkItemMenu = BookmarkItemMenu(holder.itemView.context) {
when (it) {
is BookmarkItemMenu.Item.Edit -> {
actionEmitter.onNext(BookmarkAction.Edit(tree[position]))
}
is BookmarkItemMenu.Item.Select -> {
actionEmitter.onNext(BookmarkAction.Select(tree[position]))
}
is BookmarkItemMenu.Item.Copy -> {
actionEmitter.onNext(BookmarkAction.Copy(tree[position]))
}
is BookmarkItemMenu.Item.Share -> {
actionEmitter.onNext(BookmarkAction.Share(tree[position]))
}
is BookmarkItemMenu.Item.OpenInNewTab -> {
actionEmitter.onNext(BookmarkAction.OpenInNewTab(tree[position]))
}
is BookmarkItemMenu.Item.OpenInPrivateTab -> {
actionEmitter.onNext(BookmarkAction.OpenInPrivateTab(tree[position]))
}
is BookmarkItemMenu.Item.Delete -> {
actionEmitter.onNext(BookmarkAction.Delete(tree[position]))
}
}
}
when (holder) {
is BookmarkAdapter.BookmarkItemViewHolder -> holder.bind(tree[position], bookmarkItemMenu, mode)
is BookmarkAdapter.BookmarkFolderViewHolder -> holder.bind(tree[position], bookmarkItemMenu, mode)
is BookmarkAdapter.BookmarkSeparatorViewHolder -> holder.bind(bookmarkItemMenu)
}
}
class BookmarkItemViewHolder(
view: View,
val actionEmitter: Observer<BookmarkAction>,
private val job: Job,
override val containerView: View? = view
) :
RecyclerView.ViewHolder(view), LayoutContainer, CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
private var item: BookmarkNode? = null
private var mode: BookmarkState.Mode? = BookmarkState.Mode.Normal
init {
bookmark_favicon.visibility = View.VISIBLE
bookmark_title.visibility = View.VISIBLE
bookmark_overflow.visibility = View.VISIBLE
bookmark_separator.visibility = View.GONE
bookmark_layout.isClickable = true
}
fun bind(item: BookmarkNode, bookmarkItemMenu: BookmarkItemMenu, mode: BookmarkState.Mode) {
this.item = item
this.mode = mode
bookmark_overflow.setOnClickListener {
bookmarkItemMenu.menuBuilder.build(containerView!!.context).show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN
)
}
bookmark_title.text = item.title
updateUrl(item)
}
private fun updateUrl(item: BookmarkNode) {
bookmark_layout.setOnClickListener {
if (mode == BookmarkState.Mode.Normal) {
actionEmitter.onNext(BookmarkAction.Open(item))
} else {
actionEmitter.onNext(BookmarkAction.Select(item))
}
}
bookmark_layout.setOnLongClickListener {
if (mode == BookmarkState.Mode.Normal) {
actionEmitter.onNext(BookmarkAction.Select(item))
true
} else false
}
if (item.url?.startsWith("http") == true) {
launch(Dispatchers.IO) {
val bitmap = BrowserIcons(bookmark_favicon.context)
.loadIcon(IconRequest(item.url!!)).await().bitmap
launch(Dispatchers.Main) {
bookmark_favicon.setImageBitmap(bitmap)
}
}
}
}
companion object {
val viewType = BookmarkAdapter.ViewType.ITEM
}
}
class BookmarkFolderViewHolder(
view: View,
val actionEmitter: Observer<BookmarkAction>,
override val containerView: View? = view
) :
RecyclerView.ViewHolder(view), LayoutContainer {
init {
bookmark_favicon.setImageResource(R.drawable.ic_folder_icon)
bookmark_favicon.visibility = View.VISIBLE
bookmark_title.visibility = View.VISIBLE
bookmark_overflow.visibility = View.VISIBLE
bookmark_separator.visibility = View.GONE
bookmark_layout.isClickable = true
}
fun bind(folder: BookmarkNode, bookmarkItemMenu: BookmarkItemMenu, mode: BookmarkState.Mode) {
bookmark_overflow.setOnClickListener {
bookmarkItemMenu.menuBuilder.build(containerView!!.context).show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN
)
}
bookmark_title?.text = folder.title
bookmark_layout.setOnClickListener {
actionEmitter.onNext(BookmarkAction.Expand(folder))
}
}
companion object {
val viewType = BookmarkAdapter.ViewType.FOLDER
}
}
class BookmarkSeparatorViewHolder(
view: View,
val actionEmitter: Observer<BookmarkAction>,
override val containerView: View? = view
) : RecyclerView.ViewHolder(view), LayoutContainer {
init {
bookmark_favicon.visibility = View.GONE
bookmark_title.visibility = View.GONE
bookmark_overflow.visibility = View.VISIBLE
bookmark_separator.visibility = View.VISIBLE
bookmark_layout.isClickable = false
}
fun bind(bookmarkItemMenu: BookmarkItemMenu) {
bookmark_overflow.setOnClickListener {
bookmarkItemMenu.menuBuilder.build(containerView!!.context).show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN
)
}
}
companion object {
val viewType = BookmarkAdapter.ViewType.SEPARATOR
}
}
enum class ViewType {
ITEM, FOLDER, SEPARATOR
}
}

View File

@ -0,0 +1,67 @@
/* 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.bookmarks
import android.view.ViewGroup
import mozilla.components.concept.storage.BookmarkNode
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.ViewState
class BookmarkComponent(
private val container: ViewGroup,
bus: ActionBusFactory,
override var initialState: BookmarkState =
BookmarkState(null, BookmarkState.Mode.Normal)
) :
UIComponent<BookmarkState, BookmarkAction, BookmarkChange>(
bus.getManagedEmitter(BookmarkAction::class.java),
bus.getSafeManagedObservable(BookmarkChange::class.java)
) {
override val reducer: Reducer<BookmarkState, BookmarkChange> = { state, change ->
when (change) {
is BookmarkChange.Change -> {
state.copy(tree = change.tree)
}
}
}
override fun initView(): UIView<BookmarkState, BookmarkAction, BookmarkChange> =
BookmarkUIView(container, actionEmitter, changesObservable)
init {
render(reducer)
}
}
data class BookmarkState(val tree: BookmarkNode?, val mode: BookmarkState.Mode) : ViewState {
sealed class Mode {
object Normal : Mode()
data class Selecting(val selectedItems: List<BookmarkNode>) : Mode()
}
}
sealed class BookmarkAction : Action {
data class Open(val item: BookmarkNode) : BookmarkAction()
data class Expand(val folder: BookmarkNode) : BookmarkAction()
data class Edit(val item: BookmarkNode) : BookmarkAction()
data class Copy(val item: BookmarkNode) : BookmarkAction()
data class Share(val item: BookmarkNode) : BookmarkAction()
data class OpenInNewTab(val item: BookmarkNode) : BookmarkAction()
data class OpenInPrivateTab(val item: BookmarkNode) : BookmarkAction()
data class Select(val item: BookmarkNode) : BookmarkAction()
data class Delete(val item: BookmarkNode) : BookmarkAction()
object ExitSelectMode : BookmarkAction()
object BackPressed : BookmarkAction()
}
sealed class BookmarkChange : Change {
data class Change(val tree: BookmarkNode) : BookmarkChange()
}

View File

@ -0,0 +1,175 @@
/* 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.bookmarks
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.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.fragment_bookmark.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.base.feature.BackHandler
import org.mozilla.fenix.BrowsingModeManager
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.share
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable
import org.mozilla.fenix.mvi.getManagedEmitter
import org.mozilla.fenix.utils.ItsNotBrokenSnack
import kotlin.coroutines.CoroutineContext
class BookmarkFragment : Fragment(), CoroutineScope, BackHandler {
private lateinit var job: Job
private lateinit var bookmarkComponent: BookmarkComponent
private lateinit var currentRoot: BookmarkNode
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_bookmark, container, false)
bookmarkComponent = BookmarkComponent(view.bookmark_layout, ActionBusFactory.get(this))
return view
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
setHasOptionsMenu(true)
}
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar?.show()
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.library_menu, menu)
}
@SuppressWarnings("ComplexMethod")
override fun onStart() {
super.onStart()
getAutoDisposeObservable<BookmarkAction>()
.subscribe {
when (it) {
is BookmarkAction.Open -> {
if (it.item.type == BookmarkNodeType.ITEM) {
it.item.url?.let { url ->
val activity = requireActivity() as HomeActivity
Navigation.findNavController(activity, R.id.container)
.navigate(BookmarkFragmentDirections.actionBookmarkFragmentToBrowserFragment(null))
if (activity.browsingModeManager.isPrivate) {
requireComponents.useCases.tabsUseCases.addPrivateTab.invoke(url)
activity.browsingModeManager.mode =
BrowsingModeManager.Mode.Private
} else {
requireComponents.useCases.sessionUseCases.loadUrl.invoke(url)
activity.browsingModeManager.mode =
BrowsingModeManager.Mode.Normal
}
}
}
}
is BookmarkAction.Expand -> {
Navigation.findNavController(requireActivity(), R.id.container)
.navigate(BookmarkFragmentDirections.actionBookmarkFragmentSelf(it.folder.guid))
}
is BookmarkAction.BackPressed -> {
Navigation.findNavController(requireActivity(), R.id.container).popBackStack()
}
is BookmarkAction.Edit -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1238")
}
is BookmarkAction.Select -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1239")
}
is BookmarkAction.Copy -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1239")
}
is BookmarkAction.Share -> {
it.item.url?.let { url -> requireContext().share(url) }
}
is BookmarkAction.OpenInNewTab -> {
it.item.url?.let { url ->
requireComponents.useCases.tabsUseCases.addTab.invoke(url)
(activity as HomeActivity).browsingModeManager.mode = BrowsingModeManager.Mode.Normal
}
}
is BookmarkAction.OpenInPrivateTab -> {
it.item.url?.let { url ->
requireComponents.useCases.tabsUseCases.addPrivateTab.invoke(url)
(activity as HomeActivity).browsingModeManager.mode = BrowsingModeManager.Mode.Private
}
}
is BookmarkAction.Delete -> {
launch(IO) {
requireComponents.core.bookmarksStorage.deleteNode(it.item.guid)
requireComponents.core.bookmarksStorage.getTree(currentRoot.guid, false)
?.let { node ->
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.Change(node))
}
}
}
is BookmarkAction.ExitSelectMode -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1239")
}
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.libraryClose -> {
Navigation.findNavController(requireActivity(), R.id.container)
.popBackStack(R.id.libraryFragment, true)
true
}
R.id.librarySearch -> {
// TODO Library Search
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val currentGuid = BookmarkFragmentArgs.fromBundle(arguments!!).currentRoot.ifEmpty { BookmarkRoot.Root.id }
launch(IO) {
currentRoot = requireComponents.core.bookmarksStorage.getTree(currentGuid) as BookmarkNode
launch(Main) {
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.Change(currentRoot))
}
}
}
override fun onBackPressed(): Boolean = (bookmarkComponent.uiView as BookmarkUIView).onBackPressed()
}

View File

@ -0,0 +1,56 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.bookmarks
import android.content.Context
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import org.mozilla.fenix.R
class BookmarkItemMenu(
private val context: Context,
private val onItemTapped: (BookmarkItemMenu.Item) -> Unit = {}
) {
sealed class Item {
object Edit : Item()
object Select : Item()
object Copy : Item()
object Share : Item()
object OpenInNewTab : Item()
object OpenInPrivateTab : Item()
object Delete : Item()
}
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
private val menuItems by lazy {
listOf(
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_edit_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.Edit)
},
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_select_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.Select)
},
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_copy_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.Copy)
},
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_share_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.Share)
},
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_open_in_new_tab_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.OpenInNewTab)
},
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_open_in_private_tab_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.OpenInPrivateTab)
},
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_delete_button),
textColorResource = R.color.photonRed60
) {
onItemTapped.invoke(BookmarkItemMenu.Item.Delete)
}
)
}
}

View File

@ -0,0 +1,57 @@
/* 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.bookmarks
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.functions.Consumer
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.support.base.feature.BackHandler
import org.mozilla.fenix.R
import org.mozilla.fenix.mvi.UIView
class BookmarkUIView(
container: ViewGroup,
actionEmitter: Observer<BookmarkAction>,
changesObservable: Observable<BookmarkChange>
) :
UIView<BookmarkState, BookmarkAction, BookmarkChange>(container, actionEmitter, changesObservable),
BackHandler {
var mode: BookmarkState.Mode = BookmarkState.Mode.Normal
private set
var canGoBack = false
override val view: RecyclerView = LayoutInflater.from(container.context)
.inflate(R.layout.component_bookmark, container, true)
.findViewById(R.id.bookmark_list)
private val bookmarkAdapter = BookmarkAdapter(actionEmitter)
init {
view.apply {
adapter = bookmarkAdapter
layoutManager = LinearLayoutManager(container.context)
}
}
override fun updateView() = Consumer<BookmarkState> {
canGoBack = !(listOf(null, BookmarkRoot.Root.id).contains(it.tree?.guid))
bookmarkAdapter.updateData(it.tree, it.mode)
mode = it.mode
}
override fun onBackPressed(): Boolean {
return if (canGoBack) {
actionEmitter.onNext(BookmarkAction.BackPressed)
true
} else false
}
}

View File

@ -0,0 +1,52 @@
/* 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.quickactionsheet
import android.view.ViewGroup
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.ViewState
class QuickActionComponent(
private val container: ViewGroup,
bus: ActionBusFactory,
override var initialState: QuickActionState = QuickActionState(false)
) : UIComponent<QuickActionState, QuickActionAction, QuickActionChange>(
bus.getManagedEmitter(QuickActionAction::class.java),
bus.getSafeManagedObservable(QuickActionChange::class.java)
) {
override val reducer: Reducer<QuickActionState, QuickActionChange> = { state, change ->
when (change) {
is QuickActionChange.ReadableStateChange -> {
state.copy(readable = change.readable)
}
}
}
override fun initView(): UIView<QuickActionState, QuickActionAction, QuickActionChange> =
QuickActionUIView(container, actionEmitter, changesObservable)
init {
render(reducer)
}
}
data class QuickActionState(val readable: Boolean) : ViewState
sealed class QuickActionAction : Action {
object SharePressed : QuickActionAction()
object DownloadsPressed : QuickActionAction()
object BookmarkPressed : QuickActionAction()
object ReadPressed : QuickActionAction()
}
sealed class QuickActionChange : Change {
data class ReadableStateChange(val readable: Boolean) : QuickActionChange()
}

View File

@ -67,7 +67,7 @@ class QuickActionSheet @JvmOverloads constructor(
}
private fun updateImportantForAccessibility(state: Int) {
findViewById<LinearLayout>(R.id.quick_action_sheet_buttonbar).importantForAccessibility =
findViewById<LinearLayout>(R.id.quick_action_buttons_layout).importantForAccessibility =
if (state == BottomSheetBehavior.STATE_COLLAPSED || state == BottomSheetBehavior.STATE_HIDDEN)
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
else

View File

@ -0,0 +1,55 @@
/* 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.quickactionsheet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.widget.NestedScrollView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.functions.Consumer
import kotlinx.android.synthetic.main.fragment_browser.*
import kotlinx.android.synthetic.main.layout_quick_action_sheet.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.mvi.UIView
class QuickActionUIView(
container: ViewGroup,
actionEmitter: Observer<QuickActionAction>,
changesObservable: Observable<QuickActionChange>
) : UIView<QuickActionState, QuickActionAction, QuickActionChange>(container, actionEmitter, changesObservable) {
override val view: NestedScrollView = LayoutInflater.from(container.context)
.inflate(R.layout.component_quick_action_sheet, container, true)
.findViewById(R.id.nestedScrollQuickAction) as NestedScrollView
init {
val quickActionSheetBehavior =
BottomSheetBehavior.from(nestedScrollQuickAction as View) as QuickActionSheetBehavior
view.quick_action_share.setOnClickListener {
actionEmitter.onNext(QuickActionAction.SharePressed)
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
view.quick_action_downloads.setOnClickListener {
actionEmitter.onNext(QuickActionAction.DownloadsPressed)
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
view.quick_action_bookmark.setOnClickListener {
actionEmitter.onNext(QuickActionAction.BookmarkPressed)
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
view.quick_action_read.setOnClickListener {
actionEmitter.onNext(QuickActionAction.ReadPressed)
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
override fun updateView() = Consumer<QuickActionState> {
view.quick_action_read.visibility = if (it.readable) View.VISIBLE else View.GONE
}
}

View File

@ -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/. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/bookmark_favicon_background" />
<size android:width="40dp" android:height="40dp"/>
</shape>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<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/bookmark_layout"
android:layout_width="match_parent"
android:layout_height="56dp"
android:clickable="true"
android:focusable="true"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:id="@+id/bookmark_favicon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:src="@drawable/ic_folder_icon"
tools:foregroundTint="@android:color/black"
android:padding="10dp"
android:importantForAccessibility="no"
android:background="@drawable/favicon_background"/>
<TextView
android:id="@+id/bookmark_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:lines="1"
android:textSize="16sp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/bookmark_favicon"
app:layout_constraintEnd_toStartOf="@id/bookmark_overflow"
app:layout_constraintHorizontal_bias="0"
tools:text="Internet"/>
<ImageView
android:id="@+id/bookmark_overflow"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_menu"
android:layout_margin="10dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:contentDescription="@string/bookmark_menu_content_description"/>
<View
android:id="@+id/bookmark_separator"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:importantForAccessibility="no"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/bookmark_overflow"
android:background="@android:color/black"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/bookmark_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<org.mozilla.fenix.quickactionsheet.QuickActionSheet
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/quick_action_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"/>

View File

@ -0,0 +1,12 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bookmark_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="org.mozilla.fenix.library.bookmarks.BookmarkFragment">
</LinearLayout>

View File

@ -18,19 +18,14 @@
<androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollQuickAction"
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="?attr/toolbarColor"
android:clipToPadding="true"
app:behavior_hideable="true"
app:behavior_peekHeight="15dp"
app:layout_behavior="org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior">
<org.mozilla.fenix.quickactionsheet.QuickActionSheet
android:id="@+id/quick_action_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"/>
</androidx.core.widget.NestedScrollView>
app:layout_behavior="org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior"/>
<mozilla.components.feature.findinpage.view.FindInPageBar
android:id="@+id/findInPageView"

View File

@ -21,7 +21,7 @@
android:src="@drawable/ic_drawer_pull_tab"/>
<LinearLayout
android:id="@+id/quick_action_sheet_buttonbar"
android:id="@+id/quick_action_buttons_layout"
android:orientation="horizontal"
android:layout_gravity="bottom"
android:layout_width="match_parent"
@ -29,6 +29,7 @@
android:background="?attr/toolbarColor">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/quick_action_share"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_width="0dp"
@ -40,6 +41,7 @@
android:text="@string/quick_action_share"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/quick_action_downloads"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_width="0dp"
@ -51,6 +53,7 @@
android:text="@string/quick_action_download"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/quick_action_bookmark"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_width="0dp"
@ -62,6 +65,7 @@
android:text="@string/quick_action_bookmark"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/quick_action_read"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_width="0dp"

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/selectionToolbar"
android:layout_width="match_parent"
android:layout_height="56dp"
app:titleMarginStart="16dp"
app:titleMarginEnd="16dp"
app:titleTextAppearance="@style/ToolbarTitleTextStyle"
android:background="?attr/toolbarColor"
android:elevation="8dp"/>

View File

@ -0,0 +1,15 @@
<?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/. -->
<menu 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">
<item
android:id="@+id/confirm_add_folder_button"
android:icon="@drawable/ic_new"
android:iconTint="?attr/iconColor"
android:title="@string/bookmark_add_folder"
app:showAsAction="ifRoom"
tools:targetApi="o" />
</menu>

View File

@ -0,0 +1,15 @@
<?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/. -->
<menu 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">
<item
android:id="@+id/delete_bookmark_button"
android:icon="@drawable/ic_delete"
android:iconTint="?attr/iconColor"
android:title="@string/bookmark_edit"
app:showAsAction="ifRoom"
tools:targetApi="o" />
</menu>

View File

@ -0,0 +1,15 @@
<?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/. -->
<menu 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">
<item
android:id="@+id/add_folder_button"
android:icon="@drawable/ic_new"
android:iconTint="?attr/iconColor"
android:title="@string/bookmark_select_folder"
app:showAsAction="ifRoom"
tools:targetApi="o" />
</menu>

View File

@ -10,13 +10,13 @@
android:icon="@drawable/ic_search"
android:iconTint="?attr/iconColor"
android:title="@string/library_search"
app:showAsAction="always"
app:showAsAction="ifRoom"
tools:targetApi="o" />
<item
android:id="@+id/libraryClose"
android:icon="@drawable/ic_close"
android:iconTint="?attr/iconColor"
android:title="@string/content_description_close_button"
app:showAsAction="always"
app:showAsAction="ifRoom"
tools:targetApi="o" />
</menu>

View File

@ -89,6 +89,9 @@
<action
android:id="@+id/action_libraryFragment_to_historyFragment"
app:destination="@+id/historyFragment" />
<action
android:id="@+id/action_libraryFragment_to_bookmarksFragment"
app:destination="@+id/bookmarkFragment" />
</fragment>
<fragment
@ -97,6 +100,20 @@
android:label="@string/library_history"
tools:layout="@layout/fragment_history" />
<fragment
android:id="@+id/bookmarkFragment"
android:name="org.mozilla.fenix.library.bookmarks.BookmarkFragment"
android:label="@string/library_bookmarks"
tools:layout="@layout/fragment_bookmark">
<argument
android:name="currentRoot"
app:argType="string" />
<action android:id="@+id/action_bookmarkFragment_to_browserFragment"
app:destination="@id/browserFragment" />
<action
android:id="@+id/action_bookmarkFragment_self"
app:destination="@id/bookmarkFragment" />
</fragment>
<fragment
android:id="@+id/settingsFragment"

View File

@ -8,6 +8,7 @@
<color name="color_accent">#D81B60</color>
<color name="history_delete_button_background">#F2F2F5</color>
<color name="bookmark_favicon_background">#DFDFE3</color>
<color name="light_mode_text_color">#20123A</color>
<color name="awesome_bar_title_color">#212121</color>

View File

@ -189,7 +189,7 @@
<string name="library_screenshots">Screenshots</string>
<!-- Option in Library to open Downloads page -->
<string name="library_downloads">Downloads</string>
<!-- Option in Library to open Bookmarks page -->
<!-- Option in library to open Bookmarks page -->
<string name="library_bookmarks">Bookmarks</string>
<!-- Option in Library to open History page -->
<string name="library_history">History</string>
@ -278,4 +278,32 @@
<!-- Content Description for session item share button -->
<string name="content_description_session_share">Share session</string>
<!-- Content description for bookmarks library menu -->
<string name="bookmark_menu_content_description">Bookmark menu</string>
<!-- Screen title for editing bookmarks -->
<string name="bookmark_edit">Edit bookmark</string>
<!-- Screen title for selecting a bookmarks folder -->
<string name="bookmark_select_folder">Select folder</string>
<!-- Screen title for adding a bookmarks folder -->
<string name="bookmark_add_folder">Add folder</string>
<!-- Snackbar title shown after a bookmark has been created. -->
<string name="bookmark_created_snackbar">Bookmark Created.</string>
<!-- Snackbar edit button shown after a bookmark has been created. -->
<string name="edit_bookmark_snackbar_action">EDIT</string>
<!-- Bookmark overflow menu edit button -->
<string name="bookmark_menu_edit_button">Edit</string>
<!-- Bookmark overflow menu select button -->
<string name="bookmark_menu_select_button">Select</string>
<!-- Bookmark overflow menu copy button -->
<string name="bookmark_menu_copy_button">Copy</string>
<!-- Bookmark overflow menu share button -->
<string name="bookmark_menu_share_button">Share</string>
<!-- Bookmark overflow menu open in new tab button -->
<string name="bookmark_menu_open_in_new_tab_button">Open in new tab</string>
<!-- Bookmark overflow menu open in private tab button -->
<string name="bookmark_menu_open_in_private_tab_button">Open in private tab</string>
<!-- Bookmark overflow menu delete button -->
<string name="bookmark_menu_delete_button">Delete</string>
</resources>

View File

@ -0,0 +1,71 @@
/* 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.bookmarks
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.spyk
import io.mockk.verifySequence
import io.reactivex.Observer
import io.reactivex.observers.TestObserver
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mozilla.fenix.TestUtils.setRxSchedulers
internal class BookmarkAdapterTest {
private lateinit var bookmarkAdapter: BookmarkAdapter
private lateinit var emitter: Observer<BookmarkAction>
@BeforeEach
fun setup() {
setRxSchedulers()
emitter = TestObserver<BookmarkAction>()
bookmarkAdapter = spyk(
BookmarkAdapter(emitter), recordPrivateCalls = true
)
every { bookmarkAdapter.notifyDataSetChanged() } just Runs
}
@Test
fun `update adapter from tree of bookmark nodes`() {
val tree = BookmarkNode(
BookmarkNodeType.FOLDER, "123", null, 0, "Mobile", null, listOf(
BookmarkNode(BookmarkNodeType.ITEM, "456", "123", 0, "Mozilla", "http://mozilla.org", null),
BookmarkNode(BookmarkNodeType.SEPARATOR, "789", "123", 1, null, null, null),
BookmarkNode(
BookmarkNodeType.ITEM,
"987",
"123",
2,
"Firefox",
"https://www.mozilla.org/en-US/firefox/",
null
)
)
)
bookmarkAdapter.updateData(tree, BookmarkState.Mode.Normal)
verifySequence {
bookmarkAdapter.updateData(tree, BookmarkState.Mode.Normal)
bookmarkAdapter setProperty "tree" value tree.children
bookmarkAdapter setProperty "mode" value BookmarkState.Mode.Normal
bookmarkAdapter.notifyDataSetChanged()
}
}
@Test
fun `passing null tree returns empty list`() {
bookmarkAdapter.updateData(null, BookmarkState.Mode.Normal)
verifySequence {
bookmarkAdapter.updateData(null, BookmarkState.Mode.Normal)
bookmarkAdapter setProperty "tree" value listOf<BookmarkNode?>()
bookmarkAdapter setProperty "mode" value BookmarkState.Mode.Normal
bookmarkAdapter.notifyDataSetChanged()
}
}
}

View File

@ -24,14 +24,9 @@ plugins {
allprojects {
repositories {
google()
// Currently the main repository where appservices artifacts are published.
// This will eventually move to maven.mozilla.org
// See https://github.com/mozilla/application-services/issues/252
maven {
url "https://dl.bintray.com/mozilla-appservices/application-services"
}
maven {
url "https://snapshots.maven.mozilla.org/maven2"
}

View File

@ -23,6 +23,7 @@ private object Versions {
const val appservices_gradle_plugin = "0.4.2"
const val mozilla_android_components = "0.49.0-SNAPSHOT"
const val mozilla_appservices = "0.23.0"
const val test_tools = "1.0.2"
const val espresso_core = "2.2.2"
@ -92,6 +93,8 @@ object Deps {
const val mozilla_feature_session_bundling = "org.mozilla.components:feature-session-bundling:${Versions.mozilla_android_components}"
const val mozilla_feature_site_permissions = "org.mozilla.components:feature-sitepermissions:${Versions.mozilla_android_components}"
const val mozilla_places = "org.mozilla.appservices:places:${Versions.mozilla_appservices}"
const val mozilla_service_firefox_accounts = "org.mozilla.components:service-firefox-accounts:${Versions.mozilla_android_components}"
const val mozilla_service_fretboard = "org.mozilla.components:service-fretboard:${Versions.mozilla_android_components}"
const val mozilla_service_glean = "org.mozilla.components:service-glean:${Versions.mozilla_android_components}"

View File

@ -1 +1,36 @@
include ':app', ':architecture'
//////////////////////////////////////////////////////////////////////////
// Local Development overrides
//////////////////////////////////////////////////////////////////////////
Properties localProperties = null;
String settingAppServicesPath = "substitutions.application-services.dir";
if (file('local.properties').canRead()) {
localProperties = new Properties()
localProperties.load(file('local.properties').newDataInputStream())
logger.lifecycle('Local configuration: loaded local.properties')
} else {
logger.lifecycle('Local configuration: absent local.properties; proceeding as normal.')
}
if (localProperties != null) {
String appServicesLocalPath = localProperties.getProperty(settingAppServicesPath);
if (appServicesLocalPath != null) {
logger.lifecycle("Local configuration: substituting application-services modules from path: $appServicesLocalPath")
includeBuild(appServicesLocalPath) {
dependencySubstitution {
substitute module('org.mozilla.appservices:fxaclient') with project(':fxa-client-library')
substitute module('org.mozilla.appservices:logins') with project(':logins-library')
substitute module('org.mozilla.appservices:places') with project(':places-library')
substitute module('org.mozilla.appservices:rustlog') with project(':rustlog-library')
}
}
} else {
logger.lifecycle("Local configuration: application-services substitution path missing. Specify it via '$settingAppServicesPath' setting.")
}
}