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] ## [Unreleased]
### Added ### Added
- #916 - Added the ability to save and delete bookmarks
- #356 - Adds the ability to delete history - #356 - Adds the ability to delete history
### Changed ### Changed
### Removed ### Removed

View File

@ -251,6 +251,8 @@ dependencies {
implementation Deps.leanplum implementation Deps.leanplum
implementation Deps.mozilla_places
implementation Deps.mozilla_concept_engine implementation Deps.mozilla_concept_engine
implementation Deps.mozilla_concept_storage implementation Deps.mozilla_concept_storage
implementation Deps.mozilla_concept_toolbar 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.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.search.SearchFragmentDirections import org.mozilla.fenix.search.SearchFragmentDirections
import org.mozilla.fenix.settings.SettingsFragmentDirections import org.mozilla.fenix.settings.SettingsFragmentDirections
@ -80,8 +81,8 @@ open class HomeActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// There is no session, or it has timed out; we should pop everything to home // 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) { if (components.core.sessionStorage.current() == null && !browsingModeManager.isPrivate) {
navHost.navController.popBackStack(R.id.homeFragment, false) navHost.navController.popBackStack(R.id.homeFragment, false)
} }
} }
@ -109,7 +110,6 @@ open class HomeActivity : AppCompatActivity() {
return return
} }
} }
super.onBackPressed() super.onBackPressed()
} }
@ -166,6 +166,8 @@ open class HomeActivity : AppCompatActivity() {
BrowserDirection.FromSearch -> SearchFragmentDirections.actionSearchFragmentToBrowserFragment(sessionId) BrowserDirection.FromSearch -> SearchFragmentDirections.actionSearchFragmentToBrowserFragment(sessionId)
BrowserDirection.FromSettings -> BrowserDirection.FromSettings ->
SettingsFragmentDirections.actionSettingsFragmentToBrowserFragment(sessionId) SettingsFragmentDirections.actionSettingsFragmentToBrowserFragment(sessionId)
BrowserDirection.FromBookmarks ->
BookmarkFragmentDirections.actionBookmarkFragmentToBrowserFragment(sessionId)
} }
navHost.navController.navigate(directions) navHost.navController.navigate(directions)
@ -202,5 +204,5 @@ open class HomeActivity : AppCompatActivity() {
} }
enum class BrowserDirection { 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.component_search.*
import kotlinx.android.synthetic.main.fragment_browser.view.* import kotlinx.android.synthetic.main.fragment_browser.view.*
import kotlinx.android.synthetic.main.fragment_search.* 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.browser.toolbar.behavior.BrowserToolbarBottomBehavior
import mozilla.components.feature.contextmenu.ContextMenuCandidate import mozilla.components.feature.contextmenu.ContextMenuCandidate
import mozilla.components.feature.contextmenu.ContextMenuFeature import mozilla.components.feature.contextmenu.ContextMenuFeature
@ -41,7 +46,6 @@ import org.mozilla.fenix.BrowsingModeManager
import org.mozilla.fenix.DefaultThemeManager import org.mozilla.fenix.DefaultThemeManager
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.utils.ItsNotBrokenSnack
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.FindInPageIntegration import org.mozilla.fenix.components.FindInPageIntegration
import org.mozilla.fenix.components.metrics.Event 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.ToolbarMenu
import org.mozilla.fenix.components.toolbar.ToolbarUIView import org.mozilla.fenix.components.toolbar.ToolbarUIView
import org.mozilla.fenix.customtabs.CustomTabsIntegration import org.mozilla.fenix.customtabs.CustomTabsIntegration
import org.mozilla.fenix.ext.asActivity
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.share import org.mozilla.fenix.ext.share
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable 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 import org.mozilla.fenix.utils.Settings
class BrowserFragment : Fragment(), BackHandler { class BrowserFragment : Fragment(), BackHandler {
@ -95,8 +102,12 @@ class BrowserFragment : Fragment(), BackHandler {
) )
toolbarComponent.uiView.view.apply { toolbarComponent.uiView.view.apply {
setBackgroundColor(ContextCompat.getColor(view.context, setBackgroundColor(
DefaultThemeManager.resolveAttribute(R.attr.browserToolbarBackground, context))) ContextCompat.getColor(
view.context,
DefaultThemeManager.resolveAttribute(R.attr.browserToolbarBackground, context)
)
)
(layoutParams as CoordinatorLayout.LayoutParams).apply { (layoutParams as CoordinatorLayout.LayoutParams).apply {
// Stop toolbar from collapsing if TalkBack is enabled // 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 val activity = activity as HomeActivity
DefaultThemeManager.applyStatusBarTheme(activity.window, activity.themeManager, activity, false) DefaultThemeManager.applyStatusBarTheme(activity.window, activity.themeManager, activity, false)
@ -132,10 +145,13 @@ class BrowserFragment : Fragment(), BackHandler {
ContextMenuCandidate.defaultCandidates( ContextMenuCandidate.defaultCandidates(
requireContext(), requireContext(),
requireComponents.useCases.tabsUseCases, requireComponents.useCases.tabsUseCases,
view), view
view.engineView), ),
view.engineView
),
owner = this, owner = this,
view = view) view = view
)
downloadsFeature.set( downloadsFeature.set(
feature = DownloadsFeature( feature = DownloadsFeature(
@ -146,7 +162,8 @@ class BrowserFragment : Fragment(), BackHandler {
requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS) requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS)
}), }),
owner = this, owner = this,
view = view) view = view
)
promptsFeature.set( promptsFeature.set(
feature = PromptFeature( feature = PromptFeature(
@ -157,28 +174,33 @@ class BrowserFragment : Fragment(), BackHandler {
requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS) requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS)
}), }),
owner = this, owner = this,
view = view) view = view
)
sessionFeature.set( sessionFeature.set(
feature = SessionFeature( feature = SessionFeature(
sessionManager, sessionManager,
SessionUseCases(sessionManager), SessionUseCases(sessionManager),
view.engineView, view.engineView,
sessionId), sessionId
),
owner = this, owner = this,
view = view) view = view
)
findInPageIntegration.set( findInPageIntegration.set(
feature = FindInPageIntegration( feature = FindInPageIntegration(
requireComponents.core.sessionManager, view.findInPageView, view.engineView requireComponents.core.sessionManager, view.findInPageView, view.engineView
), ),
owner = this, owner = this,
view = view) view = view
)
toolbarIntegration.set( toolbarIntegration.set(
feature = (toolbarComponent.uiView as ToolbarUIView).toolbarIntegration, feature = (toolbarComponent.uiView as ToolbarUIView).toolbarIntegration,
owner = this, owner = this,
view = view) view = view
)
sitePermissionsFeature.set( sitePermissionsFeature.set(
feature = SitePermissionsFeature( feature = SitePermissionsFeature(
@ -217,9 +239,6 @@ class BrowserFragment : Fragment(), BackHandler {
view = view view = view
) )
val actionSheet = view.findViewById<QuickActionSheet>(R.id.quick_action_sheet)
// actionSheet.afterInflate()
val actionEmitter = ActionBusFactory.get(this).getManagedEmitter(SearchAction::class.java) val actionEmitter = ActionBusFactory.get(this).getManagedEmitter(SearchAction::class.java)
sessionId?.let { id -> sessionId?.let { id ->
customTabsIntegration.set( customTabsIntegration.set(
@ -252,7 +271,8 @@ class BrowserFragment : Fragment(), BackHandler {
.findNavController(toolbarComponent.getView()) .findNavController(toolbarComponent.getView())
.navigate( .navigate(
BrowserFragmentDirections.actionBrowserFragmentToSearchFragment( BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
requireComponents.core.sessionManager.selectedSession?.id) requireComponents.core.sessionManager.selectedSession?.id
)
) )
requireComponents.analytics.metrics.track( 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() 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.engine.gecko.fetch.GeckoViewFetchClient
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.storage.SessionStorage import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
import mozilla.components.browser.storage.sync.PlacesHistoryStorage import mozilla.components.browser.storage.sync.PlacesHistoryStorage
import mozilla.components.concept.engine.DefaultSettings import mozilla.components.concept.engine.DefaultSettings
import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.Engine
@ -123,6 +124,9 @@ class Core(private val context: Context) {
*/ */
val historyStorage by lazy { PlacesHistoryStorage(context) } val historyStorage by lazy { PlacesHistoryStorage(context) }
val bookmarksStorage
by lazy { PlacesBookmarksStorage(context) }
/** /**
* Constructs a [TrackingProtectionPolicy] based on current preferences. * Constructs a [TrackingProtectionPolicy] based on current preferences.
* *

View File

@ -4,7 +4,7 @@
package org.mozilla.fenix.components.metrics package org.mozilla.fenix.components.metrics
import android.content.Context 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.service.glean.Glean
import mozilla.components.support.utils.Browsers import mozilla.components.support.utils.Browsers
import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.BuildConfig

View File

@ -15,8 +15,10 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.Navigation import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.fragment_library.* 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.R
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentArgs
import org.mozilla.fenix.utils.ItsNotBrokenSnack
class LibraryFragment : Fragment() { class LibraryFragment : Fragment() {
@ -47,15 +49,18 @@ class LibraryFragment : Fragment() {
null null
) )
) )
libraryBookmarks.setOnClickListener(Navigation.createNavigateOnClickListener(
LibraryFragmentDirections.actionLibraryFragmentToBookmarksFragment(BookmarkRoot.Root.id).actionId,
BookmarkFragmentArgs(BookmarkRoot.Root.id).toBundle()
))
libraryDownloads.setOnClickListener { libraryDownloads.setOnClickListener {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "348") ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "348")
} }
libraryScreenshots.setOnClickListener { libraryScreenshots.setOnClickListener {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "89") ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "89")
} }
libraryBookmarks.setOnClickListener {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "90")
}
libraryReadingList.setOnClickListener { libraryReadingList.setOnClickListener {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "913") 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) { 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) if (state == BottomSheetBehavior.STATE_COLLAPSED || state == BottomSheetBehavior.STATE_HIDDEN)
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
else 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 <androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollQuickAction"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="80dp" android:layout_height="80dp"
android:background="?attr/toolbarColor" android:background="?attr/toolbarColor"
android:clipToPadding="true" android:clipToPadding="true"
app:behavior_hideable="true" app:behavior_hideable="true"
app:behavior_peekHeight="15dp" app:behavior_peekHeight="15dp"
app:layout_behavior="org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior"> 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>
<mozilla.components.feature.findinpage.view.FindInPageBar <mozilla.components.feature.findinpage.view.FindInPageBar
android:id="@+id/findInPageView" android:id="@+id/findInPageView"

View File

@ -21,7 +21,7 @@
android:src="@drawable/ic_drawer_pull_tab"/> android:src="@drawable/ic_drawer_pull_tab"/>
<LinearLayout <LinearLayout
android:id="@+id/quick_action_sheet_buttonbar" android:id="@+id/quick_action_buttons_layout"
android:orientation="horizontal" android:orientation="horizontal"
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -29,6 +29,7 @@
android:background="?attr/toolbarColor"> android:background="?attr/toolbarColor">
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/quick_action_share"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:layout_width="0dp" android:layout_width="0dp"
@ -40,6 +41,7 @@
android:text="@string/quick_action_share"/> android:text="@string/quick_action_share"/>
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/quick_action_downloads"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:layout_width="0dp" android:layout_width="0dp"
@ -51,6 +53,7 @@
android:text="@string/quick_action_download"/> android:text="@string/quick_action_download"/>
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/quick_action_bookmark"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:layout_width="0dp" android:layout_width="0dp"
@ -62,6 +65,7 @@
android:text="@string/quick_action_bookmark"/> android:text="@string/quick_action_bookmark"/>
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/quick_action_read"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:layout_width="0dp" 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:icon="@drawable/ic_search"
android:iconTint="?attr/iconColor" android:iconTint="?attr/iconColor"
android:title="@string/library_search" android:title="@string/library_search"
app:showAsAction="always" app:showAsAction="ifRoom"
tools:targetApi="o" /> tools:targetApi="o" />
<item <item
android:id="@+id/libraryClose" android:id="@+id/libraryClose"
android:icon="@drawable/ic_close" android:icon="@drawable/ic_close"
android:iconTint="?attr/iconColor" android:iconTint="?attr/iconColor"
android:title="@string/content_description_close_button" android:title="@string/content_description_close_button"
app:showAsAction="always" app:showAsAction="ifRoom"
tools:targetApi="o" /> tools:targetApi="o" />
</menu> </menu>

View File

@ -89,6 +89,9 @@
<action <action
android:id="@+id/action_libraryFragment_to_historyFragment" android:id="@+id/action_libraryFragment_to_historyFragment"
app:destination="@+id/historyFragment" /> app:destination="@+id/historyFragment" />
<action
android:id="@+id/action_libraryFragment_to_bookmarksFragment"
app:destination="@+id/bookmarkFragment" />
</fragment> </fragment>
<fragment <fragment
@ -97,6 +100,20 @@
android:label="@string/library_history" android:label="@string/library_history"
tools:layout="@layout/fragment_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 <fragment
android:id="@+id/settingsFragment" android:id="@+id/settingsFragment"

View File

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

View File

@ -189,7 +189,7 @@
<string name="library_screenshots">Screenshots</string> <string name="library_screenshots">Screenshots</string>
<!-- Option in Library to open Downloads page --> <!-- Option in Library to open Downloads page -->
<string name="library_downloads">Downloads</string> <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> <string name="library_bookmarks">Bookmarks</string>
<!-- Option in Library to open History page --> <!-- Option in Library to open History page -->
<string name="library_history">History</string> <string name="library_history">History</string>
@ -278,4 +278,32 @@
<!-- Content Description for session item share button --> <!-- Content Description for session item share button -->
<string name="content_description_session_share">Share session</string> <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> </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 { allprojects {
repositories { repositories {
google() 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 { maven {
url "https://dl.bintray.com/mozilla-appservices/application-services" url "https://dl.bintray.com/mozilla-appservices/application-services"
} }
maven { maven {
url "https://snapshots.maven.mozilla.org/maven2" 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 appservices_gradle_plugin = "0.4.2"
const val mozilla_android_components = "0.49.0-SNAPSHOT" 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 test_tools = "1.0.2"
const val espresso_core = "2.2.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_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_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_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_fretboard = "org.mozilla.components:service-fretboard:${Versions.mozilla_android_components}"
const val mozilla_service_glean = "org.mozilla.components:service-glean:${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' 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.")
}
}