Commit e372ef18 authored by Severin Rudie's avatar Severin Rudie
Browse files

For Fenix#5695: adds '{app} Search' to text selection context menu

parent e0274d88
......@@ -10,9 +10,11 @@ import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.annotation.VisibleForTesting
import androidx.core.view.ViewCompat
import mozilla.components.browser.engine.gecko.selection.GeckoSelectionActionDelegate
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView
import mozilla.components.concept.engine.permission.PermissionRequest
import mozilla.components.concept.engine.selection.SelectionActionDelegate
import org.mozilla.geckoview.BasicSelectionActionDelegate
import org.mozilla.geckoview.GeckoResult
import java.lang.IllegalStateException
......@@ -75,6 +77,8 @@ class GeckoEngineView @JvmOverloads constructor(
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var currentSelection: BasicSelectionActionDelegate? = null
override var selectionActionDelegate: SelectionActionDelegate? = null
init {
// Currently this is just a FrameLayout with a single GeckoView instance. Eventually this
// implementation should handle at least two GeckoView so that we can switch between
......@@ -104,8 +108,9 @@ class GeckoEngineView @JvmOverloads constructor(
try {
currentGeckoView.setSession(internalSession.geckoSession)
currentSelection =
internalSession.geckoSession.selectionActionDelegate as? BasicSelectionActionDelegate
currentSelection = GeckoSelectionActionDelegate.maybeCreate(context, selectionActionDelegate)
?: internalSession.geckoSession.selectionActionDelegate as? BasicSelectionActionDelegate
internalSession.geckoSession.selectionActionDelegate = currentSelection
} catch (e: IllegalStateException) {
// This is to debug "display already acquired" crashes
val otherActivityClassName =
......
/* 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 mozilla.components.browser.engine.gecko.selection
import android.app.Activity
import android.content.Context
import android.view.MenuItem
import mozilla.components.concept.engine.selection.SelectionActionDelegate
import org.mozilla.geckoview.BasicSelectionActionDelegate
/**
* An adapter between the GV [BasicSelectionActionDelegate] and a generic [SelectionActionDelegate].
*
* @property customDelegate handles as much of this logic as possible.
*/
open class GeckoSelectionActionDelegate(
activity: Activity,
private val customDelegate: SelectionActionDelegate
) : BasicSelectionActionDelegate(activity) {
companion object {
/**
* @returns a [GeckoSelectionActionDelegate] if [customDelegate] is non-null and [context]
* is an instance of [Activity]. Otherwise, returns null.
*/
fun maybeCreate(context: Context, customDelegate: SelectionActionDelegate?): GeckoSelectionActionDelegate? {
return if (context !is Activity || customDelegate == null) {
null
} else {
GeckoSelectionActionDelegate(context, customDelegate)
}
}
}
override fun getAllActions(): Array<String> {
return super.getAllActions() + customDelegate.getAllActions()
}
override fun isActionAvailable(id: String): Boolean {
val customActionIsAvailable = customDelegate.isActionAvailable(id) &&
!mSelection?.text.isNullOrEmpty()
return customActionIsAvailable ||
super.isActionAvailable(id)
}
override fun prepareAction(id: String, item: MenuItem) {
val title = customDelegate.getActionTitle(id)
?: return super.prepareAction(id, item)
item.title = title
}
override fun performAction(id: String, item: MenuItem): Boolean {
val selectedText = mSelection?.text ?: return super.performAction(id, item)
return customDelegate.performAction(id, selectedText) || super.performAction(id, item)
}
}
......@@ -8,11 +8,13 @@ import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.browser.engine.gecko.selection.GeckoSelectionActionDelegate
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import mozilla.components.support.test.whenever
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.never
......@@ -179,4 +181,21 @@ class GeckoEngineViewTest {
verify(geckoView).setSession(geckoSession)
}
@Test
fun `after rendering currentSelection should be a GeckoSelectionActionDelegate`() {
val engineView = GeckoEngineView(context).apply {
selectionActionDelegate = mock()
}
val engineSession = mock<GeckoEngineSession>()
val geckoSession = mock<GeckoSession>()
val geckoView = mock<NestedGeckoView>()
whenever(engineSession.geckoSession).thenReturn(geckoSession)
engineView.currentGeckoView = geckoView
engineView.render(engineSession)
assertTrue(engineView.currentSelection is GeckoSelectionActionDelegate)
}
}
/* 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 mozilla.components.browser.engine.gecko.selection
import android.app.Activity
import android.app.Application
import android.app.Service
import mozilla.components.concept.engine.selection.SelectionActionDelegate
import mozilla.components.support.test.mock
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
class GeckoSelectionActionDelegateTest {
@Test
fun `maybe create with non-activity context should return null`() {
val customDelegate = mock<SelectionActionDelegate>()
assertNull(GeckoSelectionActionDelegate.maybeCreate(mock<Application>(), customDelegate))
assertNull(GeckoSelectionActionDelegate.maybeCreate(mock<Service>(), customDelegate))
}
@Test
fun `maybe create with null delegate context should return null`() {
assertNull(GeckoSelectionActionDelegate.maybeCreate(mock<Activity>(), null))
}
@Test
fun `maybe create with expected inputs should return non-null`() {
assertNotNull(GeckoSelectionActionDelegate.maybeCreate(mock<Activity>(), mock()))
}
@Test
fun `getAllActions should contain all actions from the custom delegate`() {
val customActions = arrayOf("1", "2", "3")
val customDelegate = object : SelectionActionDelegate {
override fun getAllActions(): Array<String> = customActions
override fun isActionAvailable(id: String): Boolean = false
override fun getActionTitle(id: String): CharSequence? = ""
override fun performAction(id: String, selectedText: String): Boolean = false
}
val geckoDelegate = TestGeckoSelectionActionDelegate(mock(), customDelegate)
val actualActions = geckoDelegate.allActions
customActions.forEach {
assertTrue(actualActions.contains(it))
}
}
}
/**
* Test object that overrides visibility for [getAllActions]
*/
class TestGeckoSelectionActionDelegate(
activity: Activity,
customDelegate: SelectionActionDelegate
) : GeckoSelectionActionDelegate(activity, customDelegate) {
public override fun getAllActions() = super.getAllActions()
}
......@@ -2,6 +2,8 @@
* 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/. */
@file:Suppress("UNUSED_PARAMETER", "UNUSED_VARIABLE")
package mozilla.components.test
import java.lang.reflect.Field
......
......@@ -15,9 +15,9 @@ import mozilla.components.browser.engine.gecko.webpush.GeckoWebPushDelegate
import mozilla.components.browser.engine.gecko.webpush.GeckoWebPushHandler
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineSession.SafeBrowsingPolicy
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory
import mozilla.components.concept.engine.EngineSession.SafeBrowsingPolicy
import mozilla.components.concept.engine.EngineSessionState
import mozilla.components.concept.engine.EngineView
import mozilla.components.concept.engine.Settings
......@@ -46,7 +46,6 @@ import org.mozilla.geckoview.GeckoRuntimeSettings
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoWebExecutor
import org.mozilla.geckoview.WebExtensionController
import java.lang.IllegalStateException
import java.util.WeakHashMap
/**
......
......@@ -10,12 +10,13 @@ import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.annotation.VisibleForTesting
import androidx.core.view.ViewCompat
import mozilla.components.browser.engine.gecko.selection.GeckoSelectionActionDelegate
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView
import mozilla.components.concept.engine.permission.PermissionRequest
import mozilla.components.concept.engine.selection.SelectionActionDelegate
import org.mozilla.geckoview.BasicSelectionActionDelegate
import org.mozilla.geckoview.GeckoResult
import java.lang.IllegalStateException
/**
* Gecko-based EngineView implementation.
......@@ -75,6 +76,8 @@ class GeckoEngineView @JvmOverloads constructor(
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var currentSelection: BasicSelectionActionDelegate? = null
override var selectionActionDelegate: SelectionActionDelegate? = null
init {
// Currently this is just a FrameLayout with a single GeckoView instance. Eventually this
// implementation should handle at least two GeckoView so that we can switch between
......@@ -104,8 +107,10 @@ class GeckoEngineView @JvmOverloads constructor(
try {
currentGeckoView.setSession(internalSession.geckoSession)
currentSelection =
internalSession.geckoSession.selectionActionDelegate as? BasicSelectionActionDelegate
currentSelection = GeckoSelectionActionDelegate.maybeCreate(context, selectionActionDelegate)
?: internalSession.geckoSession.selectionActionDelegate as? BasicSelectionActionDelegate
internalSession.geckoSession.selectionActionDelegate = currentSelection
} catch (e: IllegalStateException) {
// This is to debug "display already acquired" crashes
val otherActivityClassName =
......
/* 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 mozilla.components.browser.engine.gecko.selection
import android.app.Activity
import android.content.Context
import android.view.MenuItem
import mozilla.components.concept.engine.selection.SelectionActionDelegate
import org.mozilla.geckoview.BasicSelectionActionDelegate
/**
* An adapter between the GV [BasicSelectionActionDelegate] and a generic [SelectionActionDelegate].
*
* @param customDelegate handles as much of this logic as possible.
*/
open class GeckoSelectionActionDelegate(
activity: Activity,
private val customDelegate: SelectionActionDelegate
) : BasicSelectionActionDelegate(activity) {
companion object {
/**
* @returns a [GeckoSelectionActionDelegate] if [customDelegate] is non-null and [context]
* is an instance of [Activity]. Otherwise, returns null.
*/
fun maybeCreate(context: Context, customDelegate: SelectionActionDelegate?): GeckoSelectionActionDelegate? {
return if (context is Activity && customDelegate != null) {
GeckoSelectionActionDelegate(context, customDelegate)
} else {
null
}
}
}
override fun getAllActions(): Array<String> {
return super.getAllActions() + customDelegate.getAllActions()
}
override fun isActionAvailable(id: String): Boolean {
val customActionIsAvailable = customDelegate.isActionAvailable(id) &&
!mSelection?.text.isNullOrEmpty()
return customActionIsAvailable ||
super.isActionAvailable(id)
}
override fun prepareAction(id: String, item: MenuItem) {
val title = customDelegate.getActionTitle(id)
?: return super.prepareAction(id, item)
item.title = title
}
override fun performAction(id: String, item: MenuItem): Boolean {
val selectedText = mSelection?.text ?: return super.performAction(id, item)
return customDelegate.performAction(id, selectedText) || super.performAction(id, item)
}
}
......@@ -8,11 +8,13 @@ import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.browser.engine.gecko.selection.GeckoSelectionActionDelegate
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import mozilla.components.support.test.whenever
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.never
......@@ -179,4 +181,21 @@ class GeckoEngineViewTest {
verify(geckoView).setSession(geckoSession)
}
@Test
fun `after rendering currentSelection should be a GeckoSelectionActionDelegate`() {
val engineView = GeckoEngineView(context).apply {
selectionActionDelegate = mock()
}
val engineSession = mock<GeckoEngineSession>()
val geckoSession = mock<GeckoSession>()
val geckoView = mock<NestedGeckoView>()
whenever(engineSession.geckoSession).thenReturn(geckoSession)
engineView.currentGeckoView = geckoView
engineView.render(engineSession)
assertTrue(engineView.currentSelection is GeckoSelectionActionDelegate)
}
}
/* 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 mozilla.components.browser.engine.gecko.selection
import android.app.Activity
import android.app.Application
import android.app.Service
import mozilla.components.concept.engine.selection.SelectionActionDelegate
import mozilla.components.support.test.mock
import org.junit.Assert
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Test
class GeckoSelectionActionDelegateTest {
@Test
fun `maybe create with non-activity context should return null`() {
val customDelegate = mock<SelectionActionDelegate>()
assertNull(GeckoSelectionActionDelegate.maybeCreate(mock<Application>(), customDelegate))
assertNull(GeckoSelectionActionDelegate.maybeCreate(mock<Service>(), customDelegate))
}
@Test
fun `maybe create with null delegate context should return null`() {
assertNull(GeckoSelectionActionDelegate.maybeCreate(mock<Activity>(), null))
}
@Test
fun `maybe create with expected inputs should return non-null`() {
assertNotNull(GeckoSelectionActionDelegate.maybeCreate(mock<Activity>(), mock()))
}
@Test
fun `getAllActions should contain all actions from the custom delegate`() {
val customActions = arrayOf("1", "2", "3")
val customDelegate = object : SelectionActionDelegate {
override fun getAllActions(): Array<String> = customActions
override fun isActionAvailable(id: String): Boolean = false
override fun getActionTitle(id: String): CharSequence? = ""
override fun performAction(id: String, selectedText: String): Boolean = false
}
val geckoDelegate = TestGeckoSelectionActionDelegate(mock(), customDelegate)
val actualActions = geckoDelegate.allActions
customActions.forEach {
Assert.assertTrue(actualActions.contains(it))
}
}
}
/**
* Test object that overrides visibility for [getAllActions]
*/
class TestGeckoSelectionActionDelegate(
activity: Activity,
customDelegate: SelectionActionDelegate
) : GeckoSelectionActionDelegate(activity, customDelegate) {
public override fun getAllActions() = super.getAllActions()
}
......@@ -10,10 +10,12 @@ import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.annotation.VisibleForTesting
import androidx.core.view.ViewCompat
import mozilla.components.browser.engine.gecko.selection.GeckoSelectionActionDelegate
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView
import mozilla.components.concept.engine.permission.PermissionRequest
import org.mozilla.geckoview.BasicSelectionActionDelegate
import mozilla.components.concept.engine.selection.SelectionActionDelegate
import org.mozilla.geckoview.GeckoResult
import java.lang.IllegalStateException
......@@ -75,6 +77,8 @@ class GeckoEngineView @JvmOverloads constructor(
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var currentSelection: BasicSelectionActionDelegate? = null
override var selectionActionDelegate: SelectionActionDelegate? = null
init {
// Currently this is just a FrameLayout with a single GeckoView instance. Eventually this
// implementation should handle at least two GeckoView so that we can switch between
......@@ -104,8 +108,10 @@ class GeckoEngineView @JvmOverloads constructor(
try {
currentGeckoView.setSession(internalSession.geckoSession)
currentSelection =
internalSession.geckoSession.selectionActionDelegate as? BasicSelectionActionDelegate
currentSelection = GeckoSelectionActionDelegate.maybeCreate(context, selectionActionDelegate)
?: internalSession.geckoSession.selectionActionDelegate as? BasicSelectionActionDelegate
internalSession.geckoSession.selectionActionDelegate = currentSelection
} catch (e: IllegalStateException) {
// This is to debug "display already acquired" crashes
val otherActivityClassName =
......
/* 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 mozilla.components.browser.engine.gecko.selection
import android.app.Activity
import android.content.Context
import android.view.MenuItem
import mozilla.components.concept.engine.selection.SelectionActionDelegate
import org.mozilla.geckoview.BasicSelectionActionDelegate
/**
* An adapter between the GV [BasicSelectionActionDelegate] and a generic [SelectionActionDelegate].
*
* @property customDelegate handles as much of this logic as possible.
*/
open class GeckoSelectionActionDelegate(
activity: Activity,
private val customDelegate: SelectionActionDelegate
) : BasicSelectionActionDelegate(activity) {
companion object {
/**
* @returns a [GeckoSelectionActionDelegate] if [customDelegate] is non-null and [context]
* is an instance of [Activity]. Otherwise, returns null.
*/
fun maybeCreate(context: Context, customDelegate: SelectionActionDelegate?): GeckoSelectionActionDelegate? {
return if (context !is Activity || customDelegate == null) {
null
} else {
GeckoSelectionActionDelegate(context, customDelegate)
}
}
}
override fun getAllActions(): Array<String> {
return super.getAllActions() + customDelegate.getAllActions()
}
override fun isActionAvailable(id: String): Boolean {
val customActionIsAvailable = customDelegate.isActionAvailable(id) &&
!mSelection?.text.isNullOrEmpty()
return customActionIsAvailable ||
super.isActionAvailable(id)
}
override fun prepareAction(id: String, item: MenuItem) {
val title = customDelegate.getActionTitle(id)
?: return super.prepareAction(id, item)
item.title = title
}
override fun performAction(id: String, item: MenuItem): Boolean {
val selectedText = mSelection?.text ?: return super.performAction(id, item)
return customDelegate.performAction(id, selectedText) || super.performAction(id, item)
}
}
......@@ -8,11 +8,13 @@ import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.browser.engine.gecko.selection.GeckoSelectionActionDelegate
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import mozilla.components.support.test.whenever
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.never
......@@ -179,4 +181,21 @@ class GeckoEngineViewTest {
verify(geckoView).setSession(geckoSession)
}