diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ShareButtonTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ShareButtonTest.kt index 27bda4669..190c7cf9f 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ShareButtonTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ShareButtonTest.kt @@ -58,7 +58,7 @@ class ShareButtonTest { }.openThreeDotMenu { verifyShareButton() clickShareButton() - verifyShareDialogTitle() + verifyShareScrim() verifySendToDeviceTitle() verifyShareALinkTitle() } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuRobot.kt index af9a933eb..8a1a0101b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuRobot.kt @@ -21,6 +21,7 @@ import org.hamcrest.Matchers.allOf import org.mozilla.fenix.R import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.click +import org.mozilla.fenix.share.ShareFragment /** * Implementation of Robot Pattern for the three dot (main) menu. @@ -39,10 +40,11 @@ class ThreeDotMenuRobot { shareButton().click() mDevice.wait(Until.findObject(By.text("SHARE A LINK")), waitingTime) } + fun verifyShareTabButton() = assertShareTabButton() fun verifySaveCollection() = assertSaveCollectionButton() fun verifyFindInPageButton() = assertFindInPageButton() - fun verifyShareDialogTitle() = assertShareDialogTitle() + fun verifyShareScrim() = assertShareScrim() fun verifySendToDeviceTitle() = assertSendToDeviceTitle() fun verifyShareALinkTitle() = assertShareALinkTitle() fun verifyWhatsNewButton() = assertWhatsNewButton() @@ -128,6 +130,7 @@ class ThreeDotMenuRobot { private fun threeDotMenuRecyclerViewExists() { onView(withId(R.id.mozac_browser_menu_recyclerView)).check(matches(isDisplayed())) } + private fun settingsButton() = onView(allOf(withText(R.string.settings))) private fun assertSettingsButton() = settingsButton() .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) @@ -159,6 +162,7 @@ private fun assertCloseAllTabsButton() = closeAllTabsButton() private fun shareTabButton() = onView(allOf(withText("Share tabs"))) private fun assertShareTabButton() = shareTabButton() .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) + private fun shareButton() = onView(allOf(withText("Share"))) private fun assertShareButton() = shareButton() .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) @@ -169,15 +173,20 @@ private fun assertSaveCollectionButton() = saveCollectionButton() private fun findInPageButton() = onView(allOf(withText("Find in page"))) private fun assertFindInPageButton() = findInPageButton() -private fun ShareDialogTitle() = onView(allOf(withText("Send and Share"), withResourceName("closeButton"))) -private fun assertShareDialogTitle() = ShareDialogTitle() - .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) -private fun SendToDeviceTitle() = onView(allOf(withText("SEND TO DEVICE"), withResourceName("accountHeaderText"))) +private fun shareScrim() = onView(withResourceName("closeSharingScrim")) +private fun assertShareScrim() = + shareScrim().check(matches(ViewMatchers.withAlpha(ShareFragment.SHOW_PAGE_ALPHA))) + +private fun SendToDeviceTitle() = + onView(allOf(withText("SEND TO DEVICE"), withResourceName("accountHeaderText"))) + private fun assertSendToDeviceTitle() = SendToDeviceTitle() .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) -private fun ShareALinkTitle() = onView(allOf(withText("SHARE A LINK"), withResourceName("link_header"))) +private fun ShareALinkTitle() = + onView(allOf(withText("SHARE A LINK"), withResourceName("link_header"))) + private fun assertShareALinkTitle() = ShareALinkTitle() private fun whatsNewButton() = onView(allOf(withText("What's New"))) diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkController.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkController.kt index 345cf6a11..03bbfc5cd 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkController.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkController.kt @@ -21,6 +21,7 @@ import org.mozilla.fenix.components.Services import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.nav +import org.mozilla.fenix.share.ShareTab /** * [BookmarkFragment] controller. @@ -84,7 +85,8 @@ class DefaultBookmarkController( navigate( BookmarkFragmentDirections.actionBookmarkFragmentToShareFragment( url = item.url!!, - title = item.title + title = item.title, + tabs = arrayOf(ShareTab(item.url!!, item.title!!)) ) ) } diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt index 2e6385e45..a8ce49bcf 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt @@ -154,11 +154,15 @@ class HistoryFragment : LibraryPageFragment(), BackHandler { override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { R.id.share_history_multi_select -> { val selectedHistory = historyStore.state.mode.selectedItems + val shareTabs = selectedHistory.map { ShareTab(it.url, it.title) } when { selectedHistory.size == 1 -> - share(url = selectedHistory.first().url) + share( + url = selectedHistory.first().url, + title = selectedHistory.first().title, + tabs = shareTabs + ) selectedHistory.size > 1 -> { - val shareTabs = selectedHistory.map { ShareTab(it.url, it.title) } share(tabs = shareTabs) } } @@ -256,11 +260,12 @@ class HistoryFragment : LibraryPageFragment(), BackHandler { } } - private fun share(url: String? = null, tabs: List? = null) { + private fun share(url: String? = null, title: String? = null, tabs: List? = null) { requireComponents.analytics.metrics.track(Event.HistoryItemShared) val directions = HistoryFragmentDirections.actionHistoryFragmentToShareFragment( url = url, + title = title, tabs = tabs?.toTypedArray() ) nav(R.id.historyFragment, directions) diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareCloseView.kt b/app/src/main/java/org/mozilla/fenix/share/ShareCloseView.kt index b9a511cfc..b12ff4fbb 100644 --- a/app/src/main/java/org/mozilla/fenix/share/ShareCloseView.kt +++ b/app/src/main/java/org/mozilla/fenix/share/ShareCloseView.kt @@ -6,9 +6,11 @@ package org.mozilla.fenix.share import android.view.LayoutInflater import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.share_close.* import org.mozilla.fenix.R +import org.mozilla.fenix.share.listadapters.ShareTabsAdapter /** * Callbacks for possible user interactions on the [ShareCloseView] @@ -21,10 +23,19 @@ class ShareCloseView( override val containerView: ViewGroup, private val interactor: ShareCloseInteractor ) : LayoutContainer { + val adapter = ShareTabsAdapter() + init { LayoutInflater.from(containerView.context) .inflate(R.layout.share_close, containerView, true) closeButton.setOnClickListener { interactor.onShareClosed() } + + shared_site_list.layoutManager = LinearLayoutManager(containerView.context) + shared_site_list.adapter = adapter + } + + fun setTabs(tabs: List) { + adapter.setTabs(tabs) } } diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt b/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt index c15dddcc3..d0ae3a047 100644 --- a/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt @@ -113,9 +113,7 @@ class ShareFragment : AppCompatDialogFragment() { ): View? { val view = inflater.inflate(R.layout.fragment_share, container, false) val args = ShareFragmentArgs.fromBundle(arguments!!) - if (args.url == null && args.tabs.isNullOrEmpty()) { - throw IllegalStateException("URL and tabs cannot both be null.") - } + check(!(args.url == null && args.tabs.isNullOrEmpty())) { "URL and tabs cannot both be null." } val tabs = args.tabs?.toList() ?: listOf(ShareTab(args.url!!, args.title.orEmpty())) val accountManager = requireComponents.backgroundServices.accountManager @@ -134,7 +132,19 @@ class ShareFragment : AppCompatDialogFragment() { view.shareWrapper.setOnClickListener { shareInteractor.onShareClosed() } shareToAccountDevicesView = ShareToAccountDevicesView(view.devicesShareLayout, shareInteractor) - shareCloseView = ShareCloseView(view.closeSharingLayout, shareInteractor) + + if (args.url != null && args.tabs == null) { + // If sharing one tab from the browser fragment, show it. + // If URL is set and tabs is null, we assume the browser is visible, since navigation + // does not tell us the back stack state. + view.closeSharingScrim.alpha = SHOW_PAGE_ALPHA + view.shareWrapper.setOnClickListener { shareInteractor.onShareClosed() } + } else { + // Otherwise, show a list of tabs to share. + view.closeSharingScrim.alpha = 1.0f + shareCloseView = ShareCloseView(view.closeSharingContent, shareInteractor) + shareCloseView.setTabs(tabs) + } shareToAppsView = ShareToAppsView(view.appsShareLayout, shareInteractor) return view @@ -212,6 +222,10 @@ class ShareFragment : AppCompatDialogFragment() { } return list } + + companion object { + const val SHOW_PAGE_ALPHA = 0.6f + } } @Parcelize diff --git a/app/src/main/java/org/mozilla/fenix/share/listadapters/ShareTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/share/listadapters/ShareTabsAdapter.kt new file mode 100644 index 000000000..655c97b90 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/share/listadapters/ShareTabsAdapter.kt @@ -0,0 +1,60 @@ +/* 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.share.listadapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.share_tab_item.view.* +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.loadIntoView +import org.mozilla.fenix.share.ShareTab + +class ShareTabsAdapter : + ListAdapter(ShareTabDiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ShareTabViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.share_tab_item, parent, false) + ) + + override fun onBindViewHolder(holder: ShareTabViewHolder, position: Int) = + holder.bind(getItem(position)) + + fun setTabs(tabs: List) { + submitList(tabs.toMutableList()) + } + + inner class ShareTabViewHolder( + itemView: View + ) : RecyclerView.ViewHolder(itemView) { + + fun bind(item: ShareTab) = with(itemView) { + context.components.core.icons.loadIntoView(itemView.share_tab_favicon, item.url) + itemView.share_tab_title.text = item.title + itemView.share_tab_url.text = item.url + } + } + + private class ShareTabDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ShareTab, + newItem: ShareTab + ): Boolean { + return oldItem.url == newItem.url + } + + override fun areContentsTheSame( + oldItem: ShareTab, + newItem: ShareTab + ): Boolean { + return oldItem.url == newItem.url && oldItem.title == newItem.title + } + } +} diff --git a/app/src/main/res/drawable/bottom_sheet_dialog_fragment_background.xml b/app/src/main/res/drawable/bottom_sheet_dialog_fragment_background.xml index f22a270bf..0c9ac22a3 100644 --- a/app/src/main/res/drawable/bottom_sheet_dialog_fragment_background.xml +++ b/app/src/main/res/drawable/bottom_sheet_dialog_fragment_background.xml @@ -4,6 +4,5 @@ - - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_share.xml b/app/src/main/res/layout/fragment_share.xml index 21fa1c3f7..168f5eeef 100644 --- a/app/src/main/res/layout/fragment_share.xml +++ b/app/src/main/res/layout/fragment_share.xml @@ -10,22 +10,28 @@ android:id="@+id/shareWrapper" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@drawable/scrim_background" android:clipToPadding="false" android:fitsSystemWindows="true" tools:context="org.mozilla.fenix.share.ShareFragment"> + android:layout_height="match_parent" + android:background="@drawable/scrim_background"/> + + - + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/share_tab_item.xml b/app/src/main/res/layout/share_tab_item.xml new file mode 100644 index 000000000..47735c24f --- /dev/null +++ b/app/src/main/res/layout/share_tab_item.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7c7ae1c78..d9e692db7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -586,6 +586,8 @@ Send and Share + + Share Share diff --git a/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkControllerTest.kt b/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkControllerTest.kt index 1bb19e651..b0bb3c5df 100644 --- a/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkControllerTest.kt @@ -10,9 +10,13 @@ import android.content.Context import androidx.core.content.getSystemService import androidx.navigation.NavController import androidx.navigation.NavDestination +import androidx.navigation.NavDirections +import io.mockk.Runs import io.mockk.every +import io.mockk.just import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.slot import io.mockk.verify import io.mockk.verifyOrder import mozilla.appservices.places.BookmarkRoot @@ -21,9 +25,9 @@ import mozilla.components.concept.storage.BookmarkNodeType import org.junit.Before import org.junit.Test import org.mozilla.fenix.BrowserDirection -import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R +import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.FenixSnackbarPresenter import org.mozilla.fenix.components.Services import org.mozilla.fenix.components.metrics.Event @@ -43,13 +47,27 @@ class BookmarkControllerTest { private val homeActivity: HomeActivity = mockk(relaxed = true) private val services: Services = mockk(relaxed = true) - private val item = BookmarkNode(BookmarkNodeType.ITEM, "456", "123", 0, "Mozilla", "http://mozilla.org", null) - private val subfolder = BookmarkNode(BookmarkNodeType.FOLDER, "987", "123", 0, "Subfolder", null, listOf()) + private val item = + BookmarkNode(BookmarkNodeType.ITEM, "456", "123", 0, "Mozilla", "http://mozilla.org", null) + private val subfolder = + BookmarkNode(BookmarkNodeType.FOLDER, "987", "123", 0, "Subfolder", null, listOf()) private val childItem = BookmarkNode( - BookmarkNodeType.ITEM, "987", "123", 2, "Firefox", "https://www.mozilla.org/en-US/firefox/", null + BookmarkNodeType.ITEM, + "987", + "123", + 2, + "Firefox", + "https://www.mozilla.org/en-US/firefox/", + null ) private val tree = BookmarkNode( - BookmarkNodeType.FOLDER, "123", null, 0, "Mobile", null, listOf(item, item, childItem, subfolder) + BookmarkNodeType.FOLDER, + "123", + null, + 0, + "Mobile", + null, + listOf(item, item, childItem, subfolder) ) private val root = BookmarkNode( BookmarkNodeType.FOLDER, BookmarkRoot.Root.id, null, 0, BookmarkRoot.Root.name, null, null @@ -113,7 +131,11 @@ class BookmarkControllerTest { verify { invokePendingDeletion.invoke() - navController.navigate(BookmarkFragmentDirections.actionBookmarkFragmentToBookmarkEditFragment(item.guid)) + navController.navigate( + BookmarkFragmentDirections.actionBookmarkFragmentToBookmarkEditFragment( + item.guid + ) + ) } } @@ -145,16 +167,13 @@ class BookmarkControllerTest { @Test fun `handleBookmarkSharing should navigate to the 'Share' fragment`() { + val navDirectionsSlot = slot() + every { navController.navigate(capture(navDirectionsSlot)) } just Runs + controller.handleBookmarkSharing(item) verify { - invokePendingDeletion.invoke() - navController.navigate( - BookmarkFragmentDirections.actionBookmarkFragmentToShareFragment( - item.url, - item.title - ) - ) + navController.navigate(navDirectionsSlot.captured) } }