Skip to content
This repository has been archived by the owner on Feb 20, 2023. It is now read-only.

Commit

Permalink
For #22271 Improve URL accessing from the clipboard for Android 12 an…
Browse files Browse the repository at this point in the history
…d above.
  • Loading branch information
Amejia481 authored and mergify[bot] committed Nov 15, 2021
1 parent c42ccb4 commit 94a543a
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package org.mozilla.fenix.ui.robots

import android.net.Uri
import android.os.Build
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
Expand Down Expand Up @@ -162,10 +163,15 @@ class NavigationToolbarRobot {
waitingTime
)

mDevice.waitNotNull(
Until.findObject(By.res("org.mozilla.fenix.debug:id/clipboard_url")),
waitingTime
)
// On Android 12 or above we don't SHOW the URL unless the user requests to do so.
// See for mor information https://github.com/mozilla-mobile/fenix/issues/22271
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
mDevice.waitNotNull(
Until.findObject(By.res("org.mozilla.fenix.debug:id/clipboard_url")),
waitingTime
)
}

fillLinkButton().click()

BrowserRobot().interact()
Expand Down
66 changes: 43 additions & 23 deletions app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -288,14 +288,22 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {

binding.fillLinkFromClipboard.setOnClickListener {
requireComponents.analytics.metrics.track(Event.ClipboardSuggestionClicked)
view.hideKeyboard()
toolbarView.view.clearFocus()
(activity as HomeActivity)
.openToBrowserAndLoad(
searchTermOrURL = requireContext().components.clipboardHandler.url ?: "",
newTab = store.state.tabId == null,
from = BrowserDirection.FromSearchDialog
)
val clipboardUrl = requireContext().components.clipboardHandler.extractURL() ?: ""

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
toolbarView.view.edit.updateUrl(clipboardUrl)
hideClipboardSection()
inlineAutocompleteEditText.setSelection(clipboardUrl.length)
} else {
view.hideKeyboard()
toolbarView.view.clearFocus()
(activity as HomeActivity)
.openToBrowserAndLoad(
searchTermOrURL = clipboardUrl,
newTab = store.state.tabId == null,
from = BrowserDirection.FromSearchDialog
)
}
}

val stubListener = ViewStub.OnInflateListener { _, inflated ->
Expand Down Expand Up @@ -356,6 +364,15 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
}
}

private fun hideClipboardSection() {
binding.fillLinkFromClipboard.isVisible = false
binding.fillLinkDivider.isVisible = false
binding.pillWrapperDivider.isVisible = false
binding.clipboardUrl.isVisible = false
binding.clipboardTitle.isVisible = false
binding.linkIcon.isVisible = false
}

private fun observeSuggestionProvidersState() = consumeFlow(store) { flow ->
flow.map { state -> state.toSearchProviderState() }
.ifChanged()
Expand Down Expand Up @@ -389,12 +406,12 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
flow.map { state ->
val shouldShowView = state.showClipboardSuggestions &&
state.query.isEmpty() &&
!state.clipboardUrl.isNullOrEmpty() && !state.showSearchShortcuts
Pair(shouldShowView, state.clipboardUrl)
state.clipboardHasUrl && !state.showSearchShortcuts
Pair(shouldShowView, state.clipboardHasUrl)
}
.ifChanged()
.collect { (shouldShowView, clipboardUrl) ->
updateClipboardSuggestion(shouldShowView, clipboardUrl)
.collect { (shouldShowView) ->
updateClipboardSuggestion(shouldShowView)
}
}

Expand All @@ -418,9 +435,8 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
// We delay querying the clipboard by posting this code to the main thread message queue,
// because ClipboardManager will return null if the does app not have input focus yet.
lifecycleScope.launch(Dispatchers.Cached) {
context?.components?.clipboardHandler?.url?.let { clipboardUrl ->
store.dispatch(SearchFragmentAction.UpdateClipboardUrl(clipboardUrl))
}
val hasUrl = context?.components?.clipboardHandler?.containsURL() ?: false
store.dispatch(SearchFragmentAction.UpdateClipboardHasUrl(hasUrl))
}
}
}
Expand Down Expand Up @@ -655,25 +671,29 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
private fun isSpeechAvailable(): Boolean = speechIntent.resolveActivity(requireContext().packageManager) != null

private fun updateClipboardSuggestion(
shouldShowView: Boolean,
clipboardUrl: String?
shouldShowView: Boolean
) {
binding.fillLinkFromClipboard.isVisible = shouldShowView
binding.fillLinkDivider.isVisible = shouldShowView
binding.pillWrapperDivider.isVisible =
!(shouldShowView && requireComponents.settings.shouldUseBottomToolbar)
binding.clipboardUrl.isVisible = shouldShowView
binding.clipboardTitle.isVisible = shouldShowView
binding.linkIcon.isVisible = shouldShowView

binding.clipboardUrl.text = clipboardUrl
val contentDescription = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
"${binding.clipboardTitle.text}."
} else {
val clipboardUrl = context?.components?.clipboardHandler?.extractURL()

binding.fillLinkFromClipboard.contentDescription =
if (clipboardUrl != null && !((activity as HomeActivity).browsingModeManager.mode.isPrivate)) {
requireComponents.core.engine.speculativeConnect(clipboardUrl)
}
binding.clipboardUrl.text = clipboardUrl
binding.clipboardUrl.isVisible = shouldShowView
"${binding.clipboardTitle.text}, ${binding.clipboardUrl.text}."

if (clipboardUrl != null && !((activity as HomeActivity).browsingModeManager.mode.isPrivate)) {
requireComponents.core.engine.speculativeConnect(clipboardUrl)
}

binding.fillLinkFromClipboard.contentDescription = contentDescription
}

private fun updateToolbarContentDescription(source: SearchEngineSource) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ sealed class SearchEngineSource {
* @property showHistorySuggestions Whether or not to show history suggestions in the AwesomeBar
* @property showBookmarkSuggestions Whether or not to show the bookmark suggestion in the AwesomeBar
* @property pastedText The text pasted from the long press toolbar menu
* @property clipboardUrl The URL in the clipboard of the user - if there's any; otherwise `null`.
* @property clipboardHasUrl Indicates if the clipboard contains an URL.
*/
data class SearchFragmentState(
val query: String,
Expand All @@ -81,7 +81,7 @@ data class SearchFragmentState(
val tabId: String?,
val pastedText: String? = null,
val searchAccessPoint: Event.PerformedSearch.SearchAccessPoint?,
val clipboardUrl: String? = null
val clipboardHasUrl: Boolean = false
) : State

fun createInitialSearchFragmentState(
Expand Down Expand Up @@ -131,7 +131,7 @@ sealed class SearchFragmentAction : Action {
data class ShowSearchShortcutEnginePicker(val show: Boolean) : SearchFragmentAction()
data class AllowSearchSuggestionsInPrivateModePrompt(val show: Boolean) : SearchFragmentAction()
data class UpdateQuery(val query: String) : SearchFragmentAction()
data class UpdateClipboardUrl(val url: String?) : SearchFragmentAction()
data class UpdateClipboardHasUrl(val hasUrl: Boolean) : SearchFragmentAction()

/**
* Updates the local `SearchFragmentState` from the global `SearchState` in `BrowserStore`.
Expand Down Expand Up @@ -172,9 +172,9 @@ private fun searchStateReducer(state: SearchFragmentState, action: SearchFragmen
}
)
}
is SearchFragmentAction.UpdateClipboardUrl -> {
is SearchFragmentAction.UpdateClipboardHasUrl -> {
state.copy(
clipboardUrl = action.url
clipboardHasUrl = action.hasUrl
)
}
}
Expand Down
44 changes: 38 additions & 6 deletions app/src/main/java/org/mozilla/fenix/utils/ClipboardHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ package org.mozilla.fenix.utils
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.view.textclassifier.TextClassifier
import androidx.annotation.VisibleForTesting
import androidx.core.content.getSystemService
import mozilla.components.support.utils.SafeUrl
Expand All @@ -21,6 +23,13 @@ private const val MIME_TYPE_TEXT_HTML = "text/html"
class ClipboardHandler(val context: Context) {
private val clipboard = context.getSystemService<ClipboardManager>()!!

/**
* Provides access to the current content of the clipboard, be aware this is a sensitive
* API as from Android 12 and above, accessing it will trigger a notification letting the user
* know the app has accessed the clipboard, make sure when you call this API that users are
* completely aware that we are accessing the clipboard.
* See for more details https://github.com/mozilla-mobile/fenix/issues/22271.
*/
var text: String?
get() {
if (!clipboard.isPrimaryClipEmpty() &&
Expand All @@ -37,13 +46,36 @@ class ClipboardHandler(val context: Context) {
clipboard.setPrimaryClip(ClipData.newPlainText("Text", value))
}

val url: String?
get() {
return text?.let {
val finder = WebURLFinder(it)
finder.bestWebURL()
}
/**
* Returns a possible URL from the actual content of the clipboard, be aware this is a sensitive
* API as from Android 12 and above, accessing it will trigger a notification letting the user
* know the app has accessed the clipboard, make sure when you call this API that users are
* completely aware that we are accessing the clipboard.
* See for more details https://github.com/mozilla-mobile/fenix/issues/22271.
*/
fun extractURL(): String? {
return text?.let {
val finder = WebURLFinder(it)
finder.bestWebURL()
}
}

@Suppress("MagicNumber")
internal fun containsURL(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val description = clipboard.primaryClipDescription
// An IllegalStateException is thrown if the url is too long.
val score =
try {
description?.getConfidenceScore(TextClassifier.TYPE_URL) ?: 0F
} catch (e: IllegalStateException) {
0F
}
score >= 0.7F
} else {
!extractURL().isNullOrEmpty()
}
}

private fun ClipboardManager.isPrimaryClipPlainText() =
primaryClipDescription?.hasMimeType(MIME_TYPE_TEXT_PLAIN) ?: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ object ToolbarPopupWindow {
) {
val context = view.get()?.context ?: return
val clipboard = context.components.clipboardHandler
if (!copyVisible && clipboard.text.isNullOrEmpty()) return
if (!copyVisible && !clipboard.containsURL()) return

val isCustomTabSession = customTabId != null

Expand All @@ -54,9 +54,9 @@ object ToolbarPopupWindow {

binding.copy.isVisible = copyVisible

binding.paste.isVisible = !clipboard.text.isNullOrEmpty() && !isCustomTabSession
binding.paste.isVisible = clipboard.containsURL() && !isCustomTabSession
binding.pasteAndGo.isVisible =
!clipboard.text.isNullOrEmpty() && !isCustomTabSession
clipboard.containsURL() && !isCustomTabSession

binding.copy.setOnClickListener {
popupWindow.dismiss()
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/res/values-v31/dimens.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?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/. -->
<resources xmlns:tools="http://schemas.android.com/tools">
<dimen name="search_fragment_clipboard_item_height">49dp</dimen>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -203,16 +203,13 @@ class SearchFragmentStoreTest {
val initialState = emptyDefaultState()
val store = SearchFragmentStore(initialState)

assertNull(store.state.clipboardUrl)
assertFalse(store.state.clipboardHasUrl)

store.dispatch(
SearchFragmentAction.UpdateClipboardUrl("https://www.mozilla.org")
SearchFragmentAction.UpdateClipboardHasUrl(true)
).joinBlocking()

assertEquals(
"https://www.mozilla.org",
store.state.clipboardUrl
)
assertTrue(store.state.clipboardHasUrl)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,18 @@ class ClipboardHandlerTest {

@Test
fun getUrl() {
assertEquals(null, clipboardHandler.url)
assertEquals(null, clipboardHandler.extractURL())

clipboard.setPrimaryClip(ClipData.newPlainText("Text", clipboardUrl))
assertEquals(clipboardUrl, clipboardHandler.url)
assertEquals(clipboardUrl, clipboardHandler.extractURL())
}

@Test
fun getUrlfromTextUrlMIME() {
assertEquals(null, clipboardHandler.url)
assertEquals(null, clipboardHandler.extractURL())

clipboard.setPrimaryClip(ClipData.newHtmlText("Html", clipboardUrl, clipboardUrl))
assertEquals(clipboardUrl, clipboardHandler.url)
assertEquals(clipboardUrl, clipboardHandler.extractURL())
}

@Test
Expand Down

0 comments on commit 94a543a

Please sign in to comment.