diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e3905e58..0a06c9425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index f3e646bca..5585cb139 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index b8dea6382..ffbebb6f6 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -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 } diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index 15439fd78..70973f1a7 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -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(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() + .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(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() } diff --git a/app/src/main/java/org/mozilla/fenix/components/Core.kt b/app/src/main/java/org/mozilla/fenix/components/Core.kt index 99ca5d9fa..043675d58 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -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. * diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt index 9e64818c3..59fa45ad6 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/library/LibraryFragment.kt b/app/src/main/java/org/mozilla/fenix/library/LibraryFragment.kt index 497873f05..7ce5c1f18 100644 --- a/app/src/main/java/org/mozilla/fenix/library/LibraryFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/LibraryFragment.kt @@ -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") } diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapter.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapter.kt new file mode 100644 index 000000000..67323703c --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapter.kt @@ -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) : RecyclerView.Adapter() { + + private var tree: List = 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, + 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, + 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, + 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 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkComponent.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkComponent.kt new file mode 100644 index 000000000..b11bea8a9 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkComponent.kt @@ -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( + bus.getManagedEmitter(BookmarkAction::class.java), + bus.getSafeManagedObservable(BookmarkChange::class.java) + ) { + + override val reducer: Reducer = { state, change -> + when (change) { + is BookmarkChange.Change -> { + state.copy(tree = change.tree) + } + } + } + + override fun initView(): UIView = + 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) : 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() +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt new file mode 100644 index 000000000..3b263bdd6 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt @@ -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() + .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().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().onNext(BookmarkChange.Change(currentRoot)) + } + } + } + + override fun onBackPressed(): Boolean = (bookmarkComponent.uiView as BookmarkUIView).onBackPressed() +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkItemMenu.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkItemMenu.kt new file mode 100644 index 000000000..ceb7d6cc3 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkItemMenu.kt @@ -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) + } + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkUIView.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkUIView.kt new file mode 100644 index 000000000..00fb8515f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkUIView.kt @@ -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, + changesObservable: Observable +) : + UIView(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 { + 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 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionComponent.kt b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionComponent.kt new file mode 100644 index 000000000..e489f86fa --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionComponent.kt @@ -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( + bus.getManagedEmitter(QuickActionAction::class.java), + bus.getSafeManagedObservable(QuickActionChange::class.java) +) { + + override val reducer: Reducer = { state, change -> + when (change) { + is QuickActionChange.ReadableStateChange -> { + state.copy(readable = change.readable) + } + } + } + + override fun initView(): UIView = + 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() +} diff --git a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheet.kt b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheet.kt index 3cb5c0325..11e5aa100 100644 --- a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheet.kt +++ b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheet.kt @@ -67,7 +67,7 @@ class QuickActionSheet @JvmOverloads constructor( } private fun updateImportantForAccessibility(state: Int) { - findViewById(R.id.quick_action_sheet_buttonbar).importantForAccessibility = + findViewById(R.id.quick_action_buttons_layout).importantForAccessibility = if (state == BottomSheetBehavior.STATE_COLLAPSED || state == BottomSheetBehavior.STATE_HIDDEN) View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS else diff --git a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionUIView.kt b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionUIView.kt new file mode 100644 index 000000000..267bf9ec2 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionUIView.kt @@ -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, + changesObservable: Observable +) : UIView(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 { + view.quick_action_read.visibility = if (it.readable) View.VISIBLE else View.GONE + } +} diff --git a/app/src/main/res/drawable/favicon_background.xml b/app/src/main/res/drawable/favicon_background.xml new file mode 100644 index 000000000..5efbdace9 --- /dev/null +++ b/app/src/main/res/drawable/favicon_background.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_folder_icon.xml b/app/src/main/res/drawable/ic_folder_icon.xml new file mode 100644 index 000000000..5afbf9574 --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_icon.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/layout/bookmark_row.xml b/app/src/main/res/layout/bookmark_row.xml new file mode 100644 index 000000000..31805bfbd --- /dev/null +++ b/app/src/main/res/layout/bookmark_row.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/component_bookmark.xml b/app/src/main/res/layout/component_bookmark.xml new file mode 100644 index 000000000..5aabb2212 --- /dev/null +++ b/app/src/main/res/layout/component_bookmark.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/layout/component_quick_action_sheet.xml b/app/src/main/res/layout/component_quick_action_sheet.xml new file mode 100644 index 000000000..25ba5300e --- /dev/null +++ b/app/src/main/res/layout/component_quick_action_sheet.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_bookmark.xml b/app/src/main/res/layout/fragment_bookmark.xml new file mode 100644 index 000000000..0a9904120 --- /dev/null +++ b/app/src/main/res/layout/fragment_bookmark.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/fragment_browser.xml b/app/src/main/res/layout/fragment_browser.xml index 26013c9c9..9f1f62bc0 100644 --- a/app/src/main/res/layout/fragment_browser.xml +++ b/app/src/main/res/layout/fragment_browser.xml @@ -18,19 +18,14 @@ - - + app:layout_behavior="org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior"/> + \ No newline at end of file diff --git a/app/src/main/res/menu/bookmarks_add_folder.xml b/app/src/main/res/menu/bookmarks_add_folder.xml new file mode 100644 index 000000000..05054eb8e --- /dev/null +++ b/app/src/main/res/menu/bookmarks_add_folder.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/menu/bookmarks_edit.xml b/app/src/main/res/menu/bookmarks_edit.xml new file mode 100644 index 000000000..94a86dbb3 --- /dev/null +++ b/app/src/main/res/menu/bookmarks_edit.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/menu/bookmarks_select_folder.xml b/app/src/main/res/menu/bookmarks_select_folder.xml new file mode 100644 index 000000000..dbe8d6610 --- /dev/null +++ b/app/src/main/res/menu/bookmarks_select_folder.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/menu/library_menu.xml b/app/src/main/res/menu/library_menu.xml index acc325f6e..72ac0c3da 100644 --- a/app/src/main/res/menu/library_menu.xml +++ b/app/src/main/res/menu/library_menu.xml @@ -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" /> diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index a1c6cca81..2951a2ebd 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -89,6 +89,9 @@ + + + + + + #D81B60 #F2F2F5 + #DFDFE3 #20123A #212121 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 66e08f1af..5752363cb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -189,7 +189,7 @@ Screenshots Downloads - + Bookmarks History @@ -278,4 +278,32 @@ Share session + + + Bookmark menu + + Edit bookmark + + Select folder + + Add folder + + Bookmark Created. + + EDIT + + + Edit + + Select + + Copy + + Share + + Open in new tab + + Open in private tab + + Delete \ No newline at end of file diff --git a/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapterTest.kt new file mode 100644 index 000000000..7c221ac5a --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapterTest.kt @@ -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 + + @BeforeEach + fun setup() { + setRxSchedulers() + emitter = TestObserver() + 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() + bookmarkAdapter setProperty "mode" value BookmarkState.Mode.Normal + bookmarkAdapter.notifyDataSetChanged() + } + } +} diff --git a/build.gradle b/build.gradle index bdf3ab631..1432130b3 100644 --- a/build.gradle +++ b/build.gradle @@ -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" } diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 7ef1c1339..849ec4563 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -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}" diff --git a/settings.gradle b/settings.gradle index 4e84dceaf..aa011bd8d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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.") + } +} \ No newline at end of file