mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-11 13:11:01 +00:00
For #22271 Improve URL accessing from the clipboard for Android 12 and above.
This commit is contained in:
parent
c42ccb4966
commit
94a543a403
@ -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
|
||||
@ -162,10 +163,15 @@ class NavigationToolbarRobot {
|
||||
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()
|
||||
|
@ -288,15 +288,23 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
|
||||
|
||||
binding.fillLinkFromClipboard.setOnClickListener {
|
||||
requireComponents.analytics.metrics.track(Event.ClipboardSuggestionClicked)
|
||||
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 = requireContext().components.clipboardHandler.url ?: "",
|
||||
searchTermOrURL = clipboardUrl,
|
||||
newTab = store.state.tabId == null,
|
||||
from = BrowserDirection.FromSearchDialog
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val stubListener = ViewStub.OnInflateListener { _, inflated ->
|
||||
val searchSuggestionHintBinding = SearchSuggestionsHintBinding.bind(inflated)
|
||||
@ -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()
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
binding.fillLinkFromClipboard.contentDescription =
|
||||
"${binding.clipboardTitle.text}, ${binding.clipboardUrl.text}."
|
||||
val contentDescription = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
"${binding.clipboardTitle.text}."
|
||||
} else {
|
||||
val clipboardUrl = context?.components?.clipboardHandler?.extractURL()
|
||||
|
||||
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}."
|
||||
}
|
||||
|
||||
binding.fillLinkFromClipboard.contentDescription = contentDescription
|
||||
}
|
||||
|
||||
private fun updateToolbarContentDescription(source: SearchEngineSource) {
|
||||
|
@ -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,
|
||||
@ -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(
|
||||
@ -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`.
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -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() &&
|
||||
@ -37,14 +46,37 @@ class ClipboardHandler(val context: Context) {
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("Text", value))
|
||||
}
|
||||
|
||||
val url: String?
|
||||
get() {
|
||||
/**
|
||||
* 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
|
||||
|
||||
|
@ -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
|
||||
|
||||
@ -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()
|
||||
|
7
app/src/main/res/values-v31/dimens.xml
Normal file
7
app/src/main/res/values-v31/dimens.xml
Normal file
@ -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>
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user