Commit de93b05c authored by Colin Lee's avatar Colin Lee Committed by Sawyer Blatz
Browse files

For #2754 Add tab cards to share sheet (#5493)

* For #2754 Add tab cards to share sheet

* For #2754: Fix background near rounded corners and ShareButtonAppearanceTest

* Add license to share_tab_item
parent ac2611d7
......@@ -58,7 +58,7 @@ class ShareButtonTest {
}.openThreeDotMenu {
verifyShareButton()
clickShareButton()
verifyShareDialogTitle()
verifyShareScrim()
verifySendToDeviceTitle()
verifyShareALinkTitle()
}
......
......@@ -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")))
......
......@@ -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!!))
)
)
}
......
......@@ -154,11 +154,15 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), 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<HistoryItem>(), BackHandler {
}
}
private fun share(url: String? = null, tabs: List<ShareTab>? = null) {
private fun share(url: String? = null, title: String? = null, tabs: List<ShareTab>? = null) {
requireComponents.analytics.metrics.track(Event.HistoryItemShared)
val directions =
HistoryFragmentDirections.actionHistoryFragmentToShareFragment(
url = url,
title = title,
tabs = tabs?.toTypedArray()
)
nav(R.id.historyFragment, directions)
......
......@@ -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<ShareTab>) {
adapter.setTabs(tabs)
}
}
......@@ -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
......
/* 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<ShareTab, ShareTabsAdapter.ShareTabViewHolder>(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<ShareTab>) {
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<ShareTab>() {
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
}
}
}
......@@ -4,6 +4,5 @@
<corners
android:topLeftRadius="@dimen/bottom_sheet_corner_radius"
android:topRightRadius="@dimen/bottom_sheet_corner_radius" />
<padding android:top="@dimen/bottom_sheet_top_padding" />
<solid android:color="?colorPrimary" />
<solid android:color="?above" />
</shape>
\ No newline at end of file
......@@ -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">
<FrameLayout
android:id="@+id/closeSharingLayout"
android:id="@+id/closeSharingScrim"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
android:layout_height="match_parent"
android:background="@drawable/scrim_background"/>
<FrameLayout
android:id="@+id/closeSharingContent"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/sharingLayout"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/sharingLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bottom_sheet_dialog_fragment_background"
android:backgroundTint="?above"
app:layout_constraintBottom_toBottomOf="parent">
<FrameLayout
......
......@@ -3,22 +3,33 @@
- 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/. -->
<com.google.android.material.button.MaterialButton
<LinearLayout
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/closeButton"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/share_header"
android:textAppearance="@style/HeaderTextStyle"
android:textColor="@color/neutral_text"
android:textSize="20sp"
app:icon="@drawable/mozac_ic_close"
app:iconPadding="8dp"
app:iconTint="@color/neutral_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:backgroundTint="#000" />
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/closeButton"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/share_header_2"
android:textAppearance="@style/HeaderTextStyle"
android:textColor="@color/neutral_text"
android:textSize="20sp"
app:icon="@drawable/mozac_ic_close"
app:iconPadding="8dp"
app:iconTint="@color/neutral_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:backgroundTint="#000" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/shared_site_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:layout_margin="8dp">
<ImageView
android:id="@+id/share_tab_favicon"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="8dp"
android:background="@drawable/favicon_background"
android:backgroundTint="?above"
android:importantForAccessibility="no"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
android:id="@+id/share_tab_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textSize="20sp"
android:maxLines="1"
android:ellipsize="end"
android:textColor="?contrastText"
tools:text="Download Firefox - Free Web Browser"
app:layout_constraintStart_toEndOf="@id/share_tab_favicon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/share_tab_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="12sp"
android:maxLines="1"
android:ellipsize="end"
android:textColor="?secondaryText"
tools:text="https://www.mozilla.org/en-US/firefox/new/"
app:layout_constraintStart_toStartOf="@id/share_tab_title"
app:layout_constraintTop_toBottomOf="@id/share_tab_title"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
......@@ -586,6 +586,8 @@
<!-- Share -->
<!-- Share screen header -->
<string name="share_header">Send and Share</string>
<!-- Share screen header -->
<string name="share_header_2">Share</string>
<!-- Content description (not visible, for screen readers etc.):
"Share" button. Opens the share menu when pressed. -->
<string name="share_button_content_description">Share</string>
......
......@@ -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<NavDirections>()
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)
}
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment