Commit 51ae80b3 authored by Gela's avatar Gela Committed by Pier Angelo Vendrame
Browse files

Bug 2021964 - Clipboard in toolbar r=android-reviewers,ohall

parent 58596cb0
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -14,6 +14,17 @@ interface AccountEventsObserver {

typealias OuterDeviceCommandIncoming = DeviceCommandIncoming

/**
 * Privacy mode for tabs.
 */
enum class TabPrivacy {
    /** A private browsing tab */
    Private,

    /** A normal (non-private) tab */
    Normal,
}

/**
 * Incoming account events.
 */
+7 −1
Original line number Diff line number Diff line
@@ -230,7 +230,13 @@ class BrowserToolbarMiddleware(
            is CopyToClipboardClicked -> {
                val selectedTab = browserStore.state.selectedTab
                val url = selectedTab?.readerState?.activeUrl ?: selectedTab?.content?.url
                val isPrivate = selectedTab?.content?.private ?: true

                if (isPrivate) {
                    clipboard.sensitiveText = url
                } else {
                    clipboard.text = url
                }

                // Android 13+ shows by default a popup for copied text.
                // Avoid overlapping popups informing the user when the URL is copied to the clipboard.
+14 −8
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@ package org.mozilla.fenix.share

import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipDescription
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
@@ -16,6 +17,8 @@ import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK
import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT
import android.net.Uri
import android.os.Build
import android.os.PersistableBundle
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.core.net.toUri
import androidx.navigation.NavController
@@ -30,6 +33,7 @@ import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.sync.Device
import mozilla.components.concept.sync.FxAEntryPoint
import mozilla.components.concept.sync.TabData
import mozilla.components.concept.sync.TabPrivacy
import mozilla.components.feature.accounts.push.SendTabUseCases
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.share.RecentAppsStorage
@@ -80,6 +84,7 @@ interface ShareController {
 * @param appStore Instance of [AppStore] for interacting with application wide state.
 * @param shareSubject Desired message subject used when sharing through 3rd party apps, like email clients.
 * @param shareData The list of [ShareData]s that can be shared.
 * @param isPrivate Whether the tab(s) being shared are from private browsing mode.
 * @param sendTabUseCases Instance of [SendTabUseCases] which allows sending tabs to account devices.
 * @param saveToPdfUseCase Instance of [SessionUseCases.SaveToPdfUseCase] to generate a PDF of a given tab.
 * @param printUseCase Instance of [SessionUseCases.PrintContentUseCase] to print content of a given tab.
@@ -97,6 +102,7 @@ class DefaultShareController(
    private val appStore: AppStore,
    private val shareSubject: String?,
    private val shareData: List<ShareData>,
    private val isPrivate: Boolean,
    private val sendTabUseCases: SendTabUseCases,
    private val saveToPdfUseCase: SessionUseCases.SaveToPdfUseCase,
    private val printUseCase: SessionUseCases.PrintContentUseCase,
@@ -121,6 +127,7 @@ class DefaultShareController(
        dismiss(ShareController.Result.DISMISSED)
    }

    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    override fun handleShareToApp(app: AppShareOption) {
        Events.shareToApp.record(
            getShareToAppSafeExtra(
@@ -287,19 +294,18 @@ class DefaultShareController(
        return "data:,${Uri.encode(this)}"
    }

    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    private fun copyClipboard() {
        val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
        val clipData = ClipData.newPlainText(getShareSubject(), getShareText())

        clipboardManager.setPrimaryClip(clipData)

        // Android 13+ shows by default a popup for copied text.
        // Avoid overlapping popups informing the user when the URL is copied to the clipboard.
        // and only show our snackbar when Android will not show an indication by default.                 *
        // See https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications).
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
            appStore.dispatch(ShareAction.CopyLinkToClipboard)
        if (isPrivate) {
            clipData.description.extras = PersistableBundle().apply {
                putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
            }
        }

        clipboardManager.setPrimaryClip(clipData)
    }

    companion object {
+9 −0
Original line number Diff line number Diff line
@@ -79,12 +79,21 @@ class ShareFragment : AppCompatDialogFragment() {

        val accountManager = requireComponents.backgroundServices.accountManager

        // Determine if tabs being shared are from private browsing mode.
        // When sessionId is provided, check that specific tab's private state.
        // When sessionId is null it must be from tabs tray, and since selection mode
        // is not currently supported for private tabs, we assume it's not a private tab.
        val isPrivate = args.sessionId
            ?.let { sessionId -> requireComponents.core.store.state.findTabOrCustomTab(sessionId) }
            ?.content?.private ?: false

        shareInteractor = ShareInteractor(
            DefaultShareController(
                context = requireContext(),
                appStore = requireComponents.appStore,
                shareSubject = args.shareSubject,
                shareData = shareData,
                isPrivate = isPrivate,
                navController = findNavController(),
                sendTabUseCases = SendTabUseCases(accountManager),
                saveToPdfUseCase = requireComponents.useCases.sessionUseCases.saveToPdf,
+47 −24
Original line number Diff line number Diff line
@@ -17,9 +17,11 @@ import androidx.annotation.VisibleForTesting
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.isVisible
import mozilla.components.browser.state.selector.findCustomTab
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.utils.ClipboardHandler
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.R
@@ -81,30 +83,7 @@ object ToolbarPopupWindow {
        binding.pasteAndGo.isVisible = containsUrl && !isCustomTabSession

        if (copyVisible) {
            binding.copy.setOnClickListener { copyView ->
                popupWindow.dismiss()
                clipboard.text = getUrlForClipboard(
                    copyView.context.components.core.store,
                    customTabId,
                )

                // Android 13+ shows by default a popup for copied text.
                // Avoid overlapping popups informing the user when the URL is copied to the clipboard.
                // and only show our snackbar when Android will not show an indication by default.                 *
                // See https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications).
                if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
                    snackbarParent.get()?.let { snackbarParent ->
                        Snackbar.make(
                            snackBarParentView = snackbarParent,
                            snackbarState = SnackbarState(
                                message = context.getString(R.string.browser_toolbar_url_copied_to_clipboard_snackbar),
                                duration = SnackbarState.Duration.Preset.Long,
                            ),
                        ).show()
                    }
                }
                Events.copyUrlTapped.record(NoExtras())
            }
            setupCopyButton(binding, popupWindow, clipboard, customTabId, snackbarParent, context)
        }

        if (binding.paste.isVisible) {
@@ -137,6 +116,42 @@ object ToolbarPopupWindow {
        }
    }

    private fun setupCopyButton(
        binding: BrowserToolbarPopupWindowBinding,
        popupWindow: PopupWindow,
        clipboard: ClipboardHandler,
        customTabId: String?,
        snackbarParent: WeakReference<ViewGroup>,
        context: Context,
    ) {
        binding.copy.setOnClickListener { copyView ->
            popupWindow.dismiss()
            val store = copyView.context.components.core.store
            val url = getUrlForClipboard(store, customTabId)
            if (isPrivateTab(store, customTabId)) {
                clipboard.sensitiveText = url
            } else {
                clipboard.text = url
            }
            // Android 13+ shows by default a popup for copied text.
            // Avoid overlapping popups informing the user when the URL is copied to the clipboard.
            // and only show our snackbar when Android will not show an indication by default.
            // See https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications).
            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
                snackbarParent.get()?.let { snackbarParent ->
                    Snackbar.make(
                        snackBarParentView = snackbarParent,
                        snackbarState = SnackbarState(
                            message = context.getString(R.string.browser_toolbar_url_copied_to_clipboard_snackbar),
                            duration = SnackbarState.Duration.Preset.Long,
                        ),
                    ).show()
                }
            }
            Events.copyUrlTapped.record(NoExtras())
        }
    }

    /**
     * Calculates if the popup should be shown above or below the toolbar.
     */
@@ -167,4 +182,12 @@ object ToolbarPopupWindow {
            selectedTab?.readerState?.activeUrl ?: selectedTab?.content?.url
        }
    }

    @VisibleForTesting
    internal fun isPrivateTab(
        store: BrowserStore,
        customTabId: String? = null,
    ): Boolean {
        return store.state.findCustomTabOrSelectedTab(customTabId)?.content?.private ?: true
    }
}