Skip to content

Commit 05bbce2

Browse files
Mugurellplingurar@mozilla.com
authored andcommitted
Bug 1982573 - Show an option to copy the url when long clicking on the addressbar in custom tabs r=android-reviewers,Roger
Differential Revision: https://phabricator.services.mozilla.com/D262538
1 parent 999b6ed commit 05bbce2

File tree

5 files changed

+102
-4
lines changed

5 files changed

+102
-4
lines changed

mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/ui/Origin.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,16 +135,18 @@ internal fun Origin(
135135
Modifier.combinedClickable(
136136
role = Button,
137137
onClick = {
138-
view.playSoundEffect(SoundEffectConstants.CLICK)
139-
onInteraction(requireNotNull(onClick))
138+
onClick?.let {
139+
view.playSoundEffect(SoundEffectConstants.CLICK)
140+
onInteraction(it)
141+
}
140142
},
141143
onLongClick = {
142144
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
143145
showMenu = true
144146
onLongClick?.let { onInteraction(it) }
145147
},
146148
),
147-
) { onClick != null && shouldReactToLongClicks },
149+
) { shouldReactToLongClicks },
148150
) {
149151
Column(
150152
verticalArrangement = Center,

mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/ui/PopupToMenuItemsMapper.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package mozilla.components.compose.browser.toolbar.ui
66

77
import android.view.SoundEffectConstants
88
import androidx.compose.foundation.Image
9+
import androidx.compose.foundation.background
910
import androidx.compose.foundation.clickable
1011
import androidx.compose.foundation.interaction.MutableInteractionSource
1112
import androidx.compose.foundation.layout.Row
@@ -75,6 +76,7 @@ internal fun menuItemComposable(
7576
Row(
7677
verticalAlignment = Alignment.CenterVertically,
7778
modifier = Modifier
79+
.background(AcornTheme.colors.layer1)
7880
.thenConditional(
7981
Modifier.clickable(
8082
role = Role.Button,

mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BrowserToolbarStoreBuilder.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,13 @@ object BrowserToolbarStoreBuilder {
115115
CustomTabBrowserToolbarMiddleware(
116116
requireNotNull(customTabSession).id,
117117
browserStore = browserStore,
118+
appStore = appStore,
118119
permissionsStorage = components.core.geckoSitePermissionsStorage,
119120
cookieBannersStorage = components.core.cookieBannersStorage,
120121
useCases = components.useCases.customTabsUseCases,
121122
trackingProtectionUseCases = components.useCases.trackingProtectionUseCases,
122123
publicSuffixList = components.publicSuffixList,
124+
clipboard = activity.components.clipboardHandler,
123125
settings = settings,
124126
),
125127
)

mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/CustomTabBrowserToolbarMiddleware.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package org.mozilla.fenix.components.toolbar
66

77
import android.content.Intent
8+
import android.os.Build
89
import androidx.annotation.VisibleForTesting
910
import androidx.appcompat.content.res.AppCompatResources
1011
import androidx.core.graphics.drawable.toDrawable
@@ -26,6 +27,8 @@ import mozilla.components.compose.browser.toolbar.concept.Action
2627
import mozilla.components.compose.browser.toolbar.concept.Action.ActionButton
2728
import mozilla.components.compose.browser.toolbar.concept.Action.ActionButtonRes
2829
import mozilla.components.compose.browser.toolbar.concept.PageOrigin
30+
import mozilla.components.compose.browser.toolbar.concept.PageOrigin.Companion.ContextualMenuOption
31+
import mozilla.components.compose.browser.toolbar.concept.PageOrigin.Companion.PageOriginContextualMenuInteractions.CopyToClipboardClicked
2932
import mozilla.components.compose.browser.toolbar.store.BrowserDisplayToolbarAction
3033
import mozilla.components.compose.browser.toolbar.store.BrowserDisplayToolbarAction.BrowserActionsEndUpdated
3134
import mozilla.components.compose.browser.toolbar.store.BrowserDisplayToolbarAction.BrowserActionsStartUpdated
@@ -56,9 +59,14 @@ import mozilla.components.support.ktx.kotlin.isContentUrl
5659
import mozilla.components.support.ktx.kotlin.isIpv4OrIpv6
5760
import mozilla.components.support.ktx.kotlin.trimmed
5861
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
62+
import mozilla.components.support.utils.ClipboardHandler
63+
import mozilla.telemetry.glean.private.NoExtras
64+
import org.mozilla.fenix.GleanMetrics.Events
5965
import org.mozilla.fenix.NavGraphDirections
6066
import org.mozilla.fenix.R
6167
import org.mozilla.fenix.browser.BrowserFragmentDirections
68+
import org.mozilla.fenix.components.AppStore
69+
import org.mozilla.fenix.components.appstate.AppAction.URLCopiedToClipboard
6270
import org.mozilla.fenix.components.menu.MenuAccessPoint
6371
import org.mozilla.fenix.components.toolbar.CustomTabBrowserToolbarMiddleware.Companion.DisplayActions.MenuClicked
6472
import org.mozilla.fenix.components.toolbar.CustomTabBrowserToolbarMiddleware.Companion.DisplayActions.ShareClicked
@@ -84,23 +92,27 @@ private const val CUSTOM_BUTTON_CLICK_RETURN_CODE = 0
8492
*
8593
* @param customTabId [String] of the custom tab in which the toolbar is shown.
8694
* @param browserStore [BrowserStore] to sync from.
95+
* @param appStore [AppStore] allowing to integrate with other features of the applications.
8796
* @param permissionsStorage [SitePermissionsStorage] to sync from.
8897
* @param cookieBannersStorage [CookieBannersStorage] to sync from.
8998
* @param useCases [CustomTabsUseCases] used for cleanup when closing the custom tab.
9099
* @param trackingProtectionUseCases [TrackingProtectionUseCases] allowing to query
91100
* tracking protection data of the current tab.
92101
* @param publicSuffixList [PublicSuffixList] used to obtain the base domain of the current site.
102+
* @param clipboard [ClipboardHandler] to use for reading from device's clipboard.
93103
* @param settings [Settings] for accessing user preferences.
94104
*/
95105
@Suppress("LongParameterList")
96106
class CustomTabBrowserToolbarMiddleware(
97107
private val customTabId: String,
98108
private val browserStore: BrowserStore,
109+
private val appStore: AppStore,
99110
private val permissionsStorage: SitePermissionsStorage,
100111
private val cookieBannersStorage: CookieBannersStorage,
101112
private val useCases: CustomTabsUseCases,
102113
private val trackingProtectionUseCases: TrackingProtectionUseCases,
103114
private val publicSuffixList: PublicSuffixList,
115+
private val clipboard: ClipboardHandler,
104116
private val settings: Settings,
105117
) : Middleware<BrowserToolbarState, BrowserToolbarAction>, ViewModel() {
106118
@VisibleForTesting
@@ -245,6 +257,20 @@ class CustomTabBrowserToolbarMiddleware(
245257
}
246258
}
247259

260+
is CopyToClipboardClicked -> {
261+
Events.copyUrlTapped.record(NoExtras())
262+
263+
clipboard.text = customTab?.content?.url?.also {
264+
// Android 13+ shows by default a popup for copied text.
265+
// Avoid overlapping popups informing the user when the URL is copied to the clipboard.
266+
// and only show our snackbar when Android will not show an indication by default.
267+
// See https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications).
268+
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
269+
appStore.dispatch(URLCopiedToClipboard)
270+
}
271+
}
272+
}
273+
248274
else -> next(action)
249275
}
250276
}
@@ -312,6 +338,7 @@ class CustomTabBrowserToolbarMiddleware(
312338
hint = R.string.search_hint,
313339
title = getTitleToShown(customTab),
314340
url = getHostFromUrl()?.trimmed(),
341+
contextualMenuOptions = listOf(ContextualMenuOption.CopyURLToClipboard),
315342
onClick = null,
316343
),
317344
),

mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/CustomTabBrowserToolbarMiddlewareTest.kt

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import androidx.lifecycle.LifecycleOwner
1616
import androidx.navigation.NavController
1717
import io.mockk.every
1818
import io.mockk.mockk
19+
import io.mockk.verify
1920
import kotlinx.coroutines.CompletableDeferred
2021
import kotlinx.coroutines.test.runTest
2122
import mozilla.components.browser.state.action.ContentAction.UpdateProgressAction
@@ -26,10 +27,13 @@ import mozilla.components.browser.state.state.BrowserState
2627
import mozilla.components.browser.state.state.CustomTabSessionState
2728
import mozilla.components.browser.state.state.SecurityInfoState
2829
import mozilla.components.browser.state.state.createCustomTab
30+
import mozilla.components.browser.state.state.createTab
2931
import mozilla.components.browser.state.store.BrowserStore
3032
import mozilla.components.compose.browser.toolbar.concept.Action.ActionButton
3133
import mozilla.components.compose.browser.toolbar.concept.Action.ActionButtonRes
3234
import mozilla.components.compose.browser.toolbar.concept.PageOrigin
35+
import mozilla.components.compose.browser.toolbar.concept.PageOrigin.Companion.ContextualMenuOption
36+
import mozilla.components.compose.browser.toolbar.concept.PageOrigin.Companion.PageOriginContextualMenuInteractions.CopyToClipboardClicked
3337
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarStore
3438
import mozilla.components.compose.browser.toolbar.store.EnvironmentCleared
3539
import mozilla.components.compose.browser.toolbar.store.EnvironmentRehydrated
@@ -43,6 +47,7 @@ import mozilla.components.support.ktx.kotlin.getRegistrableDomainIndexRange
4347
import mozilla.components.support.test.ext.joinBlocking
4448
import mozilla.components.support.test.robolectric.testContext
4549
import mozilla.components.support.test.rule.MainLooperTestRule
50+
import mozilla.components.support.utils.ClipboardHandler
4651
import org.junit.Assert.assertEquals
4752
import org.junit.Assert.assertFalse
4853
import org.junit.Assert.assertNotNull
@@ -51,12 +56,16 @@ import org.junit.Assert.assertTrue
5156
import org.junit.Rule
5257
import org.junit.Test
5358
import org.junit.runner.RunWith
59+
import org.mozilla.fenix.GleanMetrics.Events
5460
import org.mozilla.fenix.R
61+
import org.mozilla.fenix.components.AppStore
62+
import org.mozilla.fenix.components.appstate.AppAction.URLCopiedToClipboard
5563
import org.mozilla.fenix.components.toolbar.CustomTabBrowserToolbarMiddleware.Companion.DisplayActions.MenuClicked
5664
import org.mozilla.fenix.components.toolbar.CustomTabBrowserToolbarMiddleware.Companion.DisplayActions.ShareClicked
5765
import org.mozilla.fenix.components.toolbar.CustomTabBrowserToolbarMiddleware.Companion.EndPageActions.CustomButtonClicked
5866
import org.mozilla.fenix.components.toolbar.CustomTabBrowserToolbarMiddleware.Companion.StartBrowserActions.CloseClicked
5967
import org.mozilla.fenix.components.toolbar.CustomTabBrowserToolbarMiddleware.Companion.StartPageActions.SiteInfoClicked
68+
import org.mozilla.fenix.helpers.FenixGleanTestRule
6069
import org.mozilla.fenix.helpers.lifecycle.TestLifecycleOwner
6170
import org.mozilla.fenix.utils.Settings
6271
import org.robolectric.RobolectricTestRunner
@@ -73,25 +82,33 @@ class CustomTabBrowserToolbarMiddlewareTest {
7382
@get:Rule
7483
val mainLooperRule = MainLooperTestRule()
7584

85+
@get:Rule
86+
val gleanRule = FenixGleanTestRule(testContext)
87+
7688
private val customTabId = "test"
7789
private val customTab: CustomTabSessionState = mockk(relaxed = true) {
7890
every { id } returns customTabId
7991
}
92+
private val selectedTab = createTab("test.com")
8093
private val browserStore = BrowserStore(
8194
BrowserState(
95+
tabs = listOf(selectedTab),
8296
customTabs = listOf(customTab),
97+
selectedTabId = selectedTab.id,
8398
),
8499
)
100+
private val appStore: AppStore = mockk()
85101
private val permissionsStorage: SitePermissionsStorage = mockk()
86102
private val cookieBannersStorage: CookieBannersStorage = mockk()
87103
private val useCases: CustomTabsUseCases = mockk()
88104
private val trackingProtectionUseCases: TrackingProtectionUseCases = mockk()
89105
private val publicSuffixList: PublicSuffixList = mockk {
90106
every { getPublicSuffixPlusOne(any()) } returns CompletableDeferred(null)
91107
}
108+
private val clipboard: ClipboardHandler = mockk()
92109
private val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.RESUMED)
93110
private val navController: NavController = mockk()
94-
private val closeTabDelegate: () -> Unit = mockk()
111+
private val closeTabDelegate: () -> Unit = {}
95112
private val settings: Settings = mockk {
96113
every { shouldUseBottomToolbar } returns true
97114
}
@@ -318,6 +335,46 @@ class CustomTabBrowserToolbarMiddlewareTest {
318335
assertEquals(expectedSecureIndicator, securityIndicator)
319336
}
320337

338+
@Test
339+
@Config(sdk = [31])
340+
fun `GIVEN on Android 12 WHEN choosing to copy the current URL to clipboard THEN copy to clipboard and show a snackbar`() {
341+
val appStore: AppStore = mockk(relaxed = true)
342+
val navController: NavController = mockk(relaxed = true)
343+
every { customTab.content.url } returns "https://mozilla.test"
344+
val clipboard = ClipboardHandler(testContext)
345+
val middleware = buildMiddleware(appStore = appStore, clipboard = clipboard)
346+
val toolbarStore = buildStore(
347+
middleware = middleware,
348+
navController = navController,
349+
)
350+
351+
toolbarStore.dispatch(CopyToClipboardClicked)
352+
353+
assertEquals(customTab.content.url, clipboard.text)
354+
verify { appStore.dispatch(URLCopiedToClipboard) }
355+
assertNotNull(Events.copyUrlTapped.testGetValue())
356+
}
357+
358+
@Test
359+
@Config(sdk = [33])
360+
fun `GIVEN on Android 13 WHEN choosing to copy the current URL to clipboard THEN copy to clipboard and don't show a snackbar`() {
361+
val appStore: AppStore = mockk(relaxed = true)
362+
val navController: NavController = mockk(relaxed = true)
363+
every { customTab.content.url } returns "https://mozilla.test"
364+
val clipboard = ClipboardHandler(testContext)
365+
val middleware = buildMiddleware(appStore = appStore, clipboard = clipboard)
366+
val toolbarStore = buildStore(
367+
middleware = middleware,
368+
navController = navController,
369+
)
370+
371+
toolbarStore.dispatch(CopyToClipboardClicked)
372+
373+
assertEquals(customTab.content.url, clipboard.text)
374+
verify(exactly = 0) { appStore.dispatch(URLCopiedToClipboard) }
375+
assertNotNull(Events.copyUrlTapped.testGetValue())
376+
}
377+
321378
@Test
322379
fun `WHEN the website title changes THEN update the shown page origin`() = runTest {
323380
val customTab = createCustomTab(title = "Title", url = "URL", id = customTabId)
@@ -329,6 +386,7 @@ class CustomTabBrowserToolbarMiddlewareTest {
329386
hint = R.string.search_hint,
330387
title = "Title",
331388
url = "URL",
389+
contextualMenuOptions = listOf(ContextualMenuOption.CopyURLToClipboard),
332390
onClick = null,
333391
)
334392

@@ -354,6 +412,7 @@ class CustomTabBrowserToolbarMiddlewareTest {
354412
hint = R.string.search_hint,
355413
title = null,
356414
url = "URL",
415+
contextualMenuOptions = listOf(ContextualMenuOption.CopyURLToClipboard),
357416
onClick = null,
358417
)
359418

@@ -379,6 +438,7 @@ class CustomTabBrowserToolbarMiddlewareTest {
379438
hint = R.string.search_hint,
380439
title = "Title",
381440
url = "URL",
441+
contextualMenuOptions = listOf(ContextualMenuOption.CopyURLToClipboard),
382442
onClick = null,
383443
)
384444

@@ -411,6 +471,7 @@ class CustomTabBrowserToolbarMiddlewareTest {
411471
hint = R.string.search_hint,
412472
title = "Title",
413473
url = "127.0.0.1",
474+
contextualMenuOptions = listOf(ContextualMenuOption.CopyURLToClipboard),
414475
onClick = null,
415476
)
416477

@@ -558,20 +619,24 @@ class CustomTabBrowserToolbarMiddlewareTest {
558619

559620
private fun buildMiddleware(
560621
browserStore: BrowserStore = this.browserStore,
622+
appStore: AppStore = this.appStore,
561623
permissionsStorage: SitePermissionsStorage = this.permissionsStorage,
562624
cookieBannersStorage: CookieBannersStorage = this.cookieBannersStorage,
563625
useCases: CustomTabsUseCases = this.useCases,
564626
trackingProtectionUseCases: TrackingProtectionUseCases = this.trackingProtectionUseCases,
565627
publicSuffixList: PublicSuffixList = this.publicSuffixList,
628+
clipboard: ClipboardHandler = this.clipboard,
566629
settings: Settings = this.settings,
567630
) = CustomTabBrowserToolbarMiddleware(
568631
customTabId = this.customTabId,
569632
browserStore = browserStore,
633+
appStore = appStore,
570634
permissionsStorage = permissionsStorage,
571635
cookieBannersStorage = cookieBannersStorage,
572636
useCases = useCases,
573637
trackingProtectionUseCases = trackingProtectionUseCases,
574638
publicSuffixList = publicSuffixList,
639+
clipboard = clipboard,
575640
settings = settings,
576641
)
577642

0 commit comments

Comments
 (0)