Bug 1914244 - Move the allow in private browsing checkbox to the web extension permissions prompt. r=willdurand,zmckenney,geckoview-reviewers,owlish,android-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D219839
This commit is contained in:
Arturo Mejia 2024-10-03 02:40:56 +00:00
parent ede8ff25ca
commit 45921cf16a
25 changed files with 554 additions and 236 deletions

View File

@ -81,6 +81,8 @@ import org.mozilla.geckoview.WebExtensionController
import org.mozilla.geckoview.WebNotification
import java.lang.ref.WeakReference
typealias NativePermissionPromptResponse = org.mozilla.geckoview.WebExtension.PermissionPromptResponse
/**
* Gecko-based implementation of Engine interface.
*/
@ -342,20 +344,26 @@ class GeckoEngine(
this.webExtensionDelegate = webExtensionDelegate
val promptDelegate = object : WebExtensionController.PromptDelegate {
override fun onInstallPrompt(
override fun onInstallPromptRequest(
ext: org.mozilla.geckoview.WebExtension,
permissions: Array<out String>,
origins: Array<out String>,
): GeckoResult<AllowOrDeny>? {
val result = GeckoResult<AllowOrDeny>()
): GeckoResult<NativePermissionPromptResponse>? {
val result = GeckoResult<NativePermissionPromptResponse>()
webExtensionDelegate.onInstallPermissionRequest(
GeckoWebExtension(ext, runtime),
// We pass both permissions and origins as a single list of
// permissions to be shown to the user.
permissions.toList() + origins.toList(),
) { allow ->
if (allow) result.complete(AllowOrDeny.ALLOW) else result.complete(AllowOrDeny.DENY)
) { data ->
result.complete(
NativePermissionPromptResponse(
data.isPermissionsGranted,
data.isPrivateModeGranted,
),
)
}
return result

View File

@ -36,6 +36,7 @@ import mozilla.components.concept.engine.translate.OperationLevel
import mozilla.components.concept.engine.utils.EngineReleaseChannel
import mozilla.components.concept.engine.webextension.Action
import mozilla.components.concept.engine.webextension.InstallationMethod
import mozilla.components.concept.engine.webextension.PermissionPromptResponse
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionDelegate
import mozilla.components.concept.engine.webextension.WebExtensionException
@ -1384,10 +1385,11 @@ class GeckoEngineTest {
val geckoDelegateCaptor = argumentCaptor<WebExtensionController.PromptDelegate>()
verify(webExtensionController).promptDelegate = geckoDelegateCaptor.capture()
val result = geckoDelegateCaptor.value.onInstallPrompt(extension, permissions, origins)
val result =
geckoDelegateCaptor.value.onInstallPromptRequest(extension, permissions, origins)
val extensionCaptor = argumentCaptor<WebExtension>()
val onConfirmCaptor = argumentCaptor<((Boolean) -> Unit)>()
val onConfirmCaptor = argumentCaptor<((PermissionPromptResponse) -> Unit)>()
verify(webExtensionsDelegate).onInstallPermissionRequest(
extensionCaptor.capture(),
@ -1395,9 +1397,66 @@ class GeckoEngineTest {
onConfirmCaptor.capture(),
)
onConfirmCaptor.value(true)
onConfirmCaptor.value(
PermissionPromptResponse(
isPermissionsGranted = true,
isPrivateModeGranted = false,
),
)
assertEquals(GeckoResult.allow(), result)
var nativePermissionPromptResponse: NativePermissionPromptResponse? = null
result!!.accept {
nativePermissionPromptResponse = it
}
shadowOf(getMainLooper()).idle()
assertTrue(nativePermissionPromptResponse!!.isPermissionsGranted!!)
assertFalse(nativePermissionPromptResponse!!.isPrivateModeGranted!!)
}
@Test
fun `GIVEN permissions granted AND private mode granted WHEN onInstallPermissionRequest THEN delegate is called with all modes allowed`() {
val runtime: GeckoRuntime = mock()
val webExtensionController: WebExtensionController = mock()
whenever(runtime.webExtensionController).thenReturn(webExtensionController)
val extension = mockNativeWebExtension("test", "uri")
val permissions = arrayOf("some", "permissions")
val origins = arrayOf("and some", "origins")
val webExtensionsDelegate: WebExtensionDelegate = mock()
val engine = GeckoEngine(context, runtime = runtime)
engine.registerWebExtensionDelegate(webExtensionsDelegate)
val geckoDelegateCaptor = argumentCaptor<WebExtensionController.PromptDelegate>()
verify(webExtensionController).promptDelegate = geckoDelegateCaptor.capture()
val result = geckoDelegateCaptor.value.onInstallPromptRequest(extension, permissions, origins)
val extensionCaptor = argumentCaptor<WebExtension>()
val onConfirmCaptor = argumentCaptor<((PermissionPromptResponse) -> Unit)>()
verify(webExtensionsDelegate).onInstallPermissionRequest(
extensionCaptor.capture(),
eq(permissions.asList() + origins.asList()),
onConfirmCaptor.capture(),
)
onConfirmCaptor.value(
PermissionPromptResponse(
isPermissionsGranted = true,
isPrivateModeGranted = true,
),
)
var nativePermissionPromptResponse: NativePermissionPromptResponse? = null
result!!.accept {
nativePermissionPromptResponse = it
}
shadowOf(getMainLooper()).idle()
assertTrue(nativePermissionPromptResponse!!.isPermissionsGranted!!)
assertTrue(nativePermissionPromptResponse!!.isPrivateModeGranted!!)
}
@Test
@ -1417,10 +1476,11 @@ class GeckoEngineTest {
val geckoDelegateCaptor = argumentCaptor<WebExtensionController.PromptDelegate>()
verify(webExtensionController).promptDelegate = geckoDelegateCaptor.capture()
val result = geckoDelegateCaptor.value.onInstallPrompt(extension, permissions, origins)
val result =
geckoDelegateCaptor.value.onInstallPromptRequest(extension, permissions, origins)
val extensionCaptor = argumentCaptor<WebExtension>()
val onConfirmCaptor = argumentCaptor<((Boolean) -> Unit)>()
val onConfirmCaptor = argumentCaptor<((PermissionPromptResponse) -> Unit)>()
verify(webExtensionsDelegate).onInstallPermissionRequest(
extensionCaptor.capture(),
@ -1428,9 +1488,21 @@ class GeckoEngineTest {
onConfirmCaptor.capture(),
)
onConfirmCaptor.value(false)
onConfirmCaptor.value(
PermissionPromptResponse(
isPermissionsGranted = false,
isPrivateModeGranted = false,
),
)
assertEquals(GeckoResult.deny(), result)
var nativePermissionPromptResponse: NativePermissionPromptResponse? = null
result!!.accept {
nativePermissionPromptResponse = it
}
shadowOf(getMainLooper()).idle()
assertFalse(nativePermissionPromptResponse!!.isPermissionsGranted!!)
assertFalse(nativePermissionPromptResponse!!.isPrivateModeGranted!!)
}
@Test

View File

@ -4,6 +4,7 @@
package mozilla.components.browser.state.state.extension
import mozilla.components.concept.engine.webextension.PermissionPromptResponse
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionInstallException
@ -43,19 +44,19 @@ sealed class WebExtensionPromptRequest {
*/
sealed class Permissions(
override val extension: WebExtension,
open val onConfirm: (Boolean) -> Unit,
) : AfterInstallation(extension) {
/**
* Value type that represents a request for a required permissions prompt.
* @property extension The [WebExtension] that requested the dialog to be shown.
* @property permissions The permissions to list in the dialog.
* @property onConfirm A callback indicating whether the permissions were granted or not.
* @property onConfirm A callback indicating the prompt has been confirmed and pass
* [PermissionPromptResponse] result.
*/
data class Required(
override val extension: WebExtension,
val permissions: List<String>,
override val onConfirm: (Boolean) -> Unit,
) : Permissions(extension, onConfirm)
val onConfirm: (PermissionPromptResponse) -> Unit,
) : Permissions(extension)
/**
* Value type that represents a request for an optional permissions prompt.
@ -66,8 +67,8 @@ sealed class WebExtensionPromptRequest {
data class Optional(
override val extension: WebExtension,
val permissions: List<String>,
override val onConfirm: (Boolean) -> Unit,
) : Permissions(extension, onConfirm)
val onConfirm: (Boolean) -> Unit,
) : Permissions(extension)
}
/**

View File

@ -519,6 +519,15 @@ enum class EnableSource(val id: Int) {
APP_SUPPORT(1 shl 1),
}
/**
* Holds all the information which the user has submitted
* as part of a confirmation of a permissions prompt request.
*/
data class PermissionPromptResponse(
val isPermissionsGranted: Boolean,
val isPrivateModeGranted: Boolean = false,
)
/**
* Flags to check for different reasons why an extension is disabled.
*/

View File

@ -115,13 +115,13 @@ interface WebExtensionDelegate {
*
* @param extension the extension being installed. The required permissions can be accessed using
* [WebExtension.getMetadata] and [Metadata.requiredPermissions]/[Metadata.requiredOrigins]/.
* @param onPermissionsGranted A callback to indicate whether the user has granted the [extension] permissions.
* @param onConfirm A callback to indicate the user's selection on the prompt.
* @return whether or not installation should process i.e. the permissions have been granted.
*/
fun onInstallPermissionRequest(
extension: WebExtension,
permissions: List<String>,
onPermissionsGranted: (Boolean) -> Unit,
onConfirm: (PermissionPromptResponse) -> Unit,
) = Unit
/**

View File

@ -20,9 +20,7 @@ import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.appcompat.widget.AppCompatCheckBox
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.R
@ -46,7 +44,7 @@ class AddonInstallationDialogFragment : AddonDialogFragment() {
/**
* A lambda called when the confirm button is clicked.
*/
var onConfirmButtonClicked: ((Addon, Boolean) -> Unit)? = null
var onConfirmButtonClicked: ((Addon) -> Unit)? = null
/**
* A lambda called when the dialog is dismissed.
@ -54,7 +52,6 @@ class AddonInstallationDialogFragment : AddonDialogFragment() {
var onDismissed: (() -> Unit)? = null
internal val addon get() = requireNotNull(safeArguments.getParcelableCompat(KEY_INSTALLED_ADDON, Addon::class.java))
private var allowPrivateBrowsing: Boolean = false
internal val confirmButtonRadius
get() =
@ -153,18 +150,9 @@ class AddonInstallationDialogFragment : AddonDialogFragment() {
loadIcon(addon = addon, iconView = binding.icon)
val allowedInPrivateBrowsing = rootView.findViewById<AppCompatCheckBox>(R.id.allow_in_private_browsing)
if (addon.incognito == Addon.Incognito.NOT_ALLOWED) {
allowedInPrivateBrowsing.isVisible = false
} else {
allowedInPrivateBrowsing.setOnCheckedChangeListener { _, isChecked ->
allowPrivateBrowsing = isChecked
}
}
val confirmButton = rootView.findViewById<Button>(R.id.confirm_button)
confirmButton.setOnClickListener {
onConfirmButtonClicked?.invoke(addon, allowPrivateBrowsing)
onConfirmButtonClicked?.invoke(addon)
dismiss()
}
@ -223,7 +211,7 @@ class AddonInstallationDialogFragment : AddonDialogFragment() {
shouldWidthMatchParent = true,
),
onDismissed: (() -> Unit)? = null,
onConfirmButtonClicked: ((Addon, Boolean) -> Unit)? = null,
onConfirmButtonClicked: ((Addon) -> Unit)? = null,
): AddonInstallationDialogFragment {
val fragment = AddonInstallationDialogFragment()
val arguments = fragment.arguments ?: Bundle()

View File

@ -20,7 +20,9 @@ import android.widget.Button
import android.widget.LinearLayout.LayoutParams
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.appcompat.widget.AppCompatCheckBox
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.feature.addons.Addon
@ -43,9 +45,10 @@ private const val DEFAULT_VALUE = Int.MAX_VALUE
class PermissionsDialogFragment : AddonDialogFragment() {
/**
* A lambda called when the allow button is clicked.
* A lambda called when the allow button is clicked which contains the [Addon] and
* whether the addon is allowed in private browsing mode.
*/
var onPositiveButtonClicked: ((Addon) -> Unit)? = null
var onPositiveButtonClicked: ((Addon, Boolean) -> Unit)? = null
/**
* A lambda called when the deny button is clicked.
@ -160,6 +163,7 @@ class PermissionsDialogFragment : AddonDialogFragment() {
val permissionsRecyclerView = rootView.findViewById<RecyclerView>(R.id.permissions)
val positiveButton = rootView.findViewById<Button>(R.id.allow_button)
val negativeButton = rootView.findViewById<Button>(R.id.deny_button)
val allowedInPrivateBrowsing = rootView.findViewById<AppCompatCheckBox>(R.id.allow_in_private_browsing)
permissionsRecyclerView.adapter = RequiredPermissionsAdapter(listPermissions)
permissionsRecyclerView.layoutManager = LinearLayoutManager(context)
@ -169,8 +173,12 @@ class PermissionsDialogFragment : AddonDialogFragment() {
negativeButton.text = requireContext().getString(R.string.mozac_feature_addons_permissions_dialog_deny)
}
if (addon.incognito == Addon.Incognito.NOT_ALLOWED) {
allowedInPrivateBrowsing.isVisible = false
}
positiveButton.setOnClickListener {
onPositiveButtonClicked?.invoke(addon)
onPositiveButtonClicked?.invoke(addon, allowedInPrivateBrowsing.isChecked)
dismiss()
}
@ -244,7 +252,7 @@ class PermissionsDialogFragment : AddonDialogFragment() {
gravity = Gravity.BOTTOM,
shouldWidthMatchParent = true,
),
onPositiveButtonClicked: ((Addon) -> Unit)? = null,
onPositiveButtonClicked: ((Addon, Boolean) -> Unit)? = null,
onNegativeButtonClicked: (() -> Unit)? = null,
): PermissionsDialogFragment {
val fragment = PermissionsDialogFragment()

View File

@ -52,24 +52,12 @@
android:textColor="?android:attr/textColorPrimary"
tools:text="@string/mozac_feature_addons_installed_dialog_description_2" />
<androidx.appcompat.widget.AppCompatCheckBox
android:id="@+id/allow_in_private_browsing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/description"
android:layout_alignStart="@id/title"
android:layout_marginTop="16dp"
android:paddingStart="5dp"
android:paddingTop="4dp"
android:paddingEnd="5dp"
android:text="@string/mozac_feature_addons_settings_allow_in_private_browsing"
android:textColor="?android:attr/textColorPrimary" />
<Button
android:id="@+id/confirm_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/allow_in_private_browsing"
android:layout_below="@id/description"
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"

View File

@ -2,11 +2,12 @@
- 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/. -->
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<RelativeLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:windowBackground"
@ -67,12 +68,25 @@
android:paddingEnd="5dp"
android:visibility="visible" />
<androidx.appcompat.widget.AppCompatCheckBox
android:id="@+id/allow_in_private_browsing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/permissions"
android:layout_alignStart="@id/title"
android:layout_marginTop="16dp"
android:paddingStart="5dp"
android:paddingTop="4dp"
android:paddingEnd="5dp"
android:text="@string/mozac_feature_addons_settings_allow_in_private_browsing"
android:textColor="?android:attr/textColorPrimary" />
<Button
android:id="@+id/deny_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/permissions"
android:layout_below="@id/allow_in_private_browsing"
android:layout_marginTop="16dp"
android:layout_toStartOf="@id/allow_button"
android:text="@string/mozac_feature_addons_permissions_dialog_cancel"
@ -83,7 +97,7 @@
android:id="@+id/allow_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/permissions"
android:layout_below="@id/allow_in_private_browsing"
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"

View File

@ -6,10 +6,7 @@ package mozilla.components.feature.addons.ui
import android.view.Gravity
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.widget.AppCompatCheckBox
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.test.ext.junit.runners.AndroidJUnit4
@ -19,7 +16,6 @@ import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
import mozilla.components.support.utils.ext.getParcelableCompat
import org.junit.Assert.assertFalse
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Rule
@ -54,53 +50,15 @@ class AddonInstallationDialogFragmentTest {
val name = addon.translateName(testContext)
val titleTextView = dialog.findViewById<TextView>(R.id.title)
val description = dialog.findViewById<TextView>(R.id.description)
val allowedInPrivateBrowsing = dialog.findViewById<AppCompatCheckBox>(R.id.allow_in_private_browsing)
assertTrue(titleTextView.text.contains(name))
assertTrue(description.text.contains(name))
assertTrue(allowedInPrivateBrowsing.isVisible)
assertTrue(allowedInPrivateBrowsing.text.contains(testContext.getString(R.string.mozac_feature_addons_settings_allow_in_private_browsing)))
}
@Test
fun `clicking on confirm dialog buttons notifies lambda with private browsing boolean`() {
val addon = Addon("id", translatableName = mapOf(Addon.DEFAULT_LOCALE to "my_addon"))
val fragment = createAddonInstallationDialogFragment(addon)
var confirmationWasExecuted = false
var allowInPrivateBrowsing = false
fragment.onConfirmButtonClicked = { _, allow ->
confirmationWasExecuted = true
allowInPrivateBrowsing = allow
}
doReturn(testContext).`when`(fragment).requireContext()
val dialog = fragment.onCreateDialog(null)
dialog.show()
val confirmButton = dialog.findViewById<Button>(R.id.confirm_button)
val allowedInPrivateBrowsing = dialog.findViewById<AppCompatCheckBox>(R.id.allow_in_private_browsing)
confirmButton.performClick()
assertTrue(confirmationWasExecuted)
assertFalse(allowInPrivateBrowsing)
dialog.show()
allowedInPrivateBrowsing.performClick()
confirmButton.performClick()
assertTrue(confirmationWasExecuted)
assertTrue(allowInPrivateBrowsing)
}
@Test
fun `dismissing the dialog notifies nothing`() {
val addon = Addon("id", translatableName = mapOf(Addon.DEFAULT_LOCALE to "my_addon"))
val fragment = createAddonInstallationDialogFragment(addon)
var confirmationWasExecuted = false
fragment.onConfirmButtonClicked = { _, _ ->
confirmationWasExecuted = true
}
doReturn(testContext).`when`(fragment).requireContext()
@ -109,7 +67,6 @@ class AddonInstallationDialogFragmentTest {
val dialog = fragment.onCreateDialog(null)
dialog.show()
fragment.onDismiss(mock())
assertFalse(confirmationWasExecuted)
}
@Test
@ -159,30 +116,6 @@ class AddonInstallationDialogFragmentTest {
verify(fragmentTransaction).commitAllowingStateLoss()
}
@Test
fun `hide private browsing checkbox when the add-on does not allow running in private windows`() {
val addon = Addon(
"id",
translatableName = mapOf(Addon.DEFAULT_LOCALE to "my_addon"),
permissions = listOf("privacy", "<all_urls>", "tabs"),
incognito = Addon.Incognito.NOT_ALLOWED,
)
val fragment = createAddonInstallationDialogFragment(addon)
assertSame(addon, fragment.arguments?.getParcelableCompat(KEY_INSTALLED_ADDON, Addon::class.java))
doReturn(testContext).`when`(fragment).requireContext()
val dialog = fragment.onCreateDialog(null)
dialog.show()
val name = addon.translateName(testContext)
val titleTextView = dialog.findViewById<TextView>(R.id.title)
val description = dialog.findViewById<TextView>(R.id.description)
val allowedInPrivateBrowsing = dialog.findViewById<AppCompatCheckBox>(R.id.allow_in_private_browsing)
assertTrue(titleTextView.text.contains(name))
assertTrue(description.text.contains(name))
assertFalse(allowedInPrivateBrowsing.isVisible)
}
private fun createAddonInstallationDialogFragment(
addon: Addon,
promptsStyling: AddonDialogFragment.PromptsStyling? = null,

View File

@ -8,6 +8,8 @@ import android.view.Gravity.TOP
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.widget.AppCompatCheckBox
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.recyclerview.widget.RecyclerView
@ -17,9 +19,11 @@ import mozilla.components.feature.addons.R
import mozilla.components.feature.addons.ui.AddonDialogFragment.PromptsStyling
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.utils.ext.getParcelableCompat
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@ -50,12 +54,15 @@ class PermissionsDialogFragmentTest {
val recyclerAdapter = permissionsRecyclerView.adapter!! as RequiredPermissionsAdapter
val permissionList = fragment.buildPermissionsList()
val optionalOrRequiredText = fragment.buildOptionalOrRequiredText(hasPermissions = permissionList.isNotEmpty())
val allowedInPrivateBrowsing =
dialog.findViewById<AppCompatCheckBox>(R.id.allow_in_private_browsing)
assertTrue(titleTextView.text.contains(name))
assertTrue(optionalOrRequiredText.contains(testContext.getString(R.string.mozac_feature_addons_permissions_dialog_subtitle)))
assertTrue(permissionList.contains(testContext.getString(R.string.mozac_feature_addons_permissions_privacy_description)))
assertTrue(permissionList.contains(testContext.getString(R.string.mozac_feature_addons_permissions_all_urls_description)))
assertTrue(permissionList.contains(testContext.getString(R.string.mozac_feature_addons_permissions_tabs_description)))
assertTrue(allowedInPrivateBrowsing.isVisible)
assertTrue(optionalOrRequiredTextView.text.contains(testContext.getString(R.string.mozac_feature_addons_permissions_dialog_subtitle)))
Assert.assertNotNull(recyclerAdapter)
@ -73,7 +80,7 @@ class PermissionsDialogFragmentTest {
var allowedWasExecuted = false
var denyWasExecuted = false
fragment.onPositiveButtonClicked = {
fragment.onPositiveButtonClicked = { _, _ ->
allowedWasExecuted = true
}
@ -220,6 +227,37 @@ class PermissionsDialogFragmentTest {
assertEquals(denyButton.text, testContext.getString(R.string.mozac_feature_addons_permissions_dialog_deny))
}
@Test
fun `hide private browsing checkbox when the add-on does not allow running in private windows`() {
val permissions = listOf("privacy", "<all_urls>", "tabs")
val addon = Addon(
"id",
translatableName = mapOf(Addon.DEFAULT_LOCALE to "my_addon"),
permissions = permissions,
incognito = Addon.Incognito.NOT_ALLOWED,
)
val fragment = createPermissionsDialogFragment(addon, permissions)
assertSame(
addon,
fragment.arguments?.getParcelableCompat(KEY_ADDON, Addon::class.java),
)
doReturn(testContext).`when`(fragment).requireContext()
val dialog = fragment.onCreateDialog(null)
dialog.show()
val name = addon.translateName(testContext)
val titleTextView = dialog.findViewById<TextView>(R.id.title)
val allowedInPrivateBrowsing =
dialog.findViewById<AppCompatCheckBox>(R.id.allow_in_private_browsing)
assertTrue(titleTextView.text.contains(name))
assertFalse(allowedInPrivateBrowsing.isVisible)
}
private fun createPermissionsDialogFragment(
addon: Addon,
permissions: List<String>,

View File

@ -28,6 +28,7 @@ import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.webextension.Action
import mozilla.components.concept.engine.webextension.ActionHandler
import mozilla.components.concept.engine.webextension.PermissionPromptResponse
import mozilla.components.concept.engine.webextension.TabHandler
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionDelegate
@ -295,14 +296,14 @@ object WebExtensionSupport {
override fun onInstallPermissionRequest(
extension: WebExtension,
permissions: List<String>,
onPermissionsGranted: (Boolean) -> Unit,
onConfirm: (PermissionPromptResponse) -> Unit,
) {
store.dispatch(
WebExtensionAction.UpdatePromptRequestWebExtensionAction(
WebExtensionPromptRequest.AfterInstallation.Permissions.Required(
extension,
permissions,
onPermissionsGranted,
onConfirm,
),
),
)

View File

@ -23,6 +23,7 @@ import mozilla.components.concept.engine.webextension.Action
import mozilla.components.concept.engine.webextension.ActionHandler
import mozilla.components.concept.engine.webextension.Incognito
import mozilla.components.concept.engine.webextension.Metadata
import mozilla.components.concept.engine.webextension.PermissionPromptResponse
import mozilla.components.concept.engine.webextension.TabHandler
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionDelegate
@ -443,7 +444,7 @@ class WebExtensionSupportTest {
val store = spy(BrowserStore())
val engine: Engine = mock()
val ext: WebExtension = mock()
val onPermissionsGranted: ((Boolean) -> Unit) = mock()
val onPermissionsGranted: ((PermissionPromptResponse) -> Unit) = mock()
val permissions = listOf("permissions")
val delegateCaptor = argumentCaptor<WebExtensionDelegate>()

View File

@ -68,10 +68,6 @@ class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate {
findPreviousPermissionDialogFragment()?.let { dialog ->
dialog.onPositiveButtonClicked = onConfirmPermissionButtonClicked
}
findPreviousInstallationDialogFragment()?.let { dialog ->
dialog.onConfirmButtonClicked = onConfirmInstallationButtonClicked
}
}
private fun bindRecyclerView(rootView: View) {
@ -174,7 +170,6 @@ class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate {
}
val dialog = AddonInstallationDialogFragment.newInstance(
addon = addon,
onConfirmButtonClicked = onConfirmInstallationButtonClicked,
)
if (!isAlreadyADialogCreated() && isAdded) {
@ -182,16 +177,7 @@ class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate {
}
}
private val onConfirmInstallationButtonClicked: ((Addon, Boolean) -> Unit) = { addon, allowInPrivateBrowsing ->
if (allowInPrivateBrowsing) {
requireContext().components.addonManager.setAddonAllowedInPrivateBrowsing(
addon,
allowInPrivateBrowsing,
)
}
}
private val onConfirmPermissionButtonClicked: ((Addon) -> Unit) = { addon ->
private val onConfirmPermissionButtonClicked: ((Addon, Boolean) -> Unit) = { addon, _ ->
val includedBinding = OverlayAddOnProgressBinding.bind(binding.addonProgressOverlay.addonProgressOverlay)
includedBinding.root.visibility = View.VISIBLE

View File

@ -4,6 +4,7 @@
package org.mozilla.fenix.ui
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
@ -130,14 +131,14 @@ class SettingsAddonsTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/561594
@SmokeTest
@Ignore("Intermittent test https://bugzilla.mozilla.org/show_bug.cgi?id=1827180")
@Test
fun verifyUBlockWorksInPrivateModeTest() {
TestHelper.appContext.settings().shouldShowCookieBannersCFR = false
val addonName = "uBlock Origin"
addonsMenu {
installAddon(addonName, activityTestRule)
selectAllowInPrivateBrowsing()
installAddonInPrivateMode(addonName, activityTestRule)
closeAddonInstallCompletePrompt()
}.goBack {
}.openContextMenuOnSponsoredShortcut("Top Articles") {

View File

@ -144,7 +144,6 @@ class SettingsSubMenuAddonsManagerRobot {
withParent(instanceOf(RelativeLayout::class.java)),
hasSibling(withText("$addonName has been added to $appName")),
hasSibling(withText("Access $addonName from the $appName menu.")),
hasSibling(withText("Allow in private browsing")),
),
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
@ -242,6 +241,18 @@ class SettingsSubMenuAddonsManagerRobot {
}
}
fun installAddonInPrivateMode(addonName: String, activityTestRule: HomeActivityIntentTestRule) {
homeScreen {
}.openThreeDotMenu {
}.openAddonsManagerMenu {
clickInstallAddon(addonName)
verifyAddonPermissionPrompt(addonName)
selectAllowInPrivateBrowsing()
acceptPermissionToInstallAddon()
verifyAddonInstallCompleted(addonName, activityTestRule)
}
}
class Transition {
fun goBack(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
Log.i(TAG, "goBack: Trying to click navigate up toolbar button")

View File

@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.mapNotNull
import mozilla.components.browser.state.action.WebExtensionAction
import mozilla.components.browser.state.state.extension.WebExtensionPromptRequest
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.webextension.PermissionPromptResponse
import mozilla.components.concept.engine.webextension.WebExtensionInstallException
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonManager
@ -29,7 +30,6 @@ import mozilla.components.ui.widgets.withCenterAlignedButtons
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.theme.ThemeManager
import java.lang.ref.WeakReference
/**
* Feature implementation for handling [WebExtensionPromptRequest] and showing the respective UI.
@ -133,7 +133,7 @@ class WebExtensionPromptFeature(
// If we don't have any promptable permissions, just proceed.
if (shouldGrantWithoutPrompt) {
handlePermissions(promptRequest, granted = true)
handlePermissions(promptRequest, granted = true, privateBrowsingAllowed = false)
return
}
@ -247,8 +247,20 @@ class WebExtensionPromptFeature(
confirmButtonRadius =
(context.resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat(),
),
onPositiveButtonClicked = { handlePermissions(promptRequest, granted = true) },
onNegativeButtonClicked = { handlePermissions(promptRequest, granted = false) },
onPositiveButtonClicked = { _, privateBrowsingAllowed ->
handlePermissions(
promptRequest,
granted = true,
privateBrowsingAllowed,
)
},
onNegativeButtonClicked = {
handlePermissions(
promptRequest,
granted = false,
privateBrowsingAllowed = false,
)
},
)
dialog.show(
fragmentManager,
@ -258,38 +270,25 @@ class WebExtensionPromptFeature(
private fun tryToReAttachButtonHandlersToPreviousDialog() {
findPreviousPermissionDialogFragment()?.let { dialog ->
dialog.onPositiveButtonClicked = { addon ->
dialog.onPositiveButtonClicked = { addon, privateBrowsingAllowed ->
store.state.webExtensionPromptRequest?.let { promptRequest ->
if (promptRequest is WebExtensionPromptRequest.AfterInstallation.Permissions &&
addon.id == promptRequest.extension.id
) {
handlePermissions(promptRequest, granted = true)
handlePermissions(promptRequest, granted = true, privateBrowsingAllowed)
}
}
}
dialog.onNegativeButtonClicked = {
store.state.webExtensionPromptRequest?.let { promptRequest ->
if (promptRequest is WebExtensionPromptRequest.AfterInstallation.Permissions) {
handlePermissions(promptRequest, granted = false)
handlePermissions(promptRequest, granted = false, privateBrowsingAllowed = false)
}
}
}
}
findPreviousPostInstallationDialogFragment()?.let { dialog ->
dialog.onConfirmButtonClicked = { addon, allowInPrivateBrowsing ->
store.state.webExtensionPromptRequest?.let { promptRequest ->
if (promptRequest is WebExtensionPromptRequest.AfterInstallation.PostInstallation &&
addon.id == promptRequest.extension.id
) {
handlePostInstallationButtonClicked(
allowInPrivateBrowsing = allowInPrivateBrowsing,
context = WeakReference(context),
addon = addon,
)
}
}
}
dialog.onDismissed = {
store.state.webExtensionPromptRequest?.let { _ ->
consumePromptRequest()
@ -301,8 +300,21 @@ class WebExtensionPromptFeature(
private fun handlePermissions(
promptRequest: WebExtensionPromptRequest.AfterInstallation.Permissions,
granted: Boolean,
privateBrowsingAllowed: Boolean,
) {
promptRequest.onConfirm(granted)
when (promptRequest) {
is WebExtensionPromptRequest.AfterInstallation.Permissions.Optional -> {
promptRequest.onConfirm(granted)
}
is WebExtensionPromptRequest.AfterInstallation.Permissions.Required -> {
val response = PermissionPromptResponse(
isPermissionsGranted = granted,
isPrivateModeGranted = privateBrowsingAllowed,
)
promptRequest.onConfirm(response)
}
}
consumePromptRequest()
}
@ -332,13 +344,6 @@ class WebExtensionPromptFeature(
private fun showPostInstallationDialog(addon: Addon) {
if (!isInstallationInProgress && !hasExistingAddonPostInstallationDialogFragment()) {
// Fragment may not be attached to the context anymore during onConfirmButtonClicked handling,
// but we still want to be able to process user selection of the 'allowInPrivateBrowsing' pref.
// This is a best-effort attempt to do so - retain a weak reference to the application context
// (to avoid a leak), which we attempt to use to access addonManager.
// See https://github.com/mozilla-mobile/fenix/issues/15816
val weakApplicationContext: WeakReference<Context> = WeakReference(context)
val dialog = AddonInstallationDialogFragment.newInstance(
addon = addon,
promptsStyling = AddonDialogFragment.PromptsStyling(
@ -358,35 +363,14 @@ class WebExtensionPromptFeature(
onDismissed = {
consumePromptRequest()
},
onConfirmButtonClicked = { _, allowInPrivateBrowsing ->
handlePostInstallationButtonClicked(
addon = addon,
context = weakApplicationContext,
allowInPrivateBrowsing = allowInPrivateBrowsing,
)
onConfirmButtonClicked = { _ ->
consumePromptRequest()
},
)
dialog.show(fragmentManager, POST_INSTALLATION_DIALOG_FRAGMENT_TAG)
}
}
private fun handlePostInstallationButtonClicked(
context: WeakReference<Context>,
allowInPrivateBrowsing: Boolean,
addon: Addon,
) {
if (allowInPrivateBrowsing) {
context.get()?.components?.addonManager?.setAddonAllowedInPrivateBrowsing(
addon = addon,
allowed = true,
onSuccess = { updatedAddon ->
onAddonChanged(updatedAddon)
},
)
}
consumePromptRequest()
}
@VisibleForTesting
internal fun showDialog(
title: String,

View File

@ -2708,6 +2708,12 @@ package org.mozilla.geckoview {
field @NonNull public final String version;
}
public static class WebExtension.PermissionPromptResponse {
ctor public PermissionPromptResponse(@Nullable Boolean, @Nullable Boolean);
field @Nullable public final Boolean isPermissionsGranted;
field @Nullable public final Boolean isPrivateModeGranted;
}
@UiThread public static class WebExtension.Port {
ctor protected Port();
method public void disconnect();
@ -2829,7 +2835,8 @@ package org.mozilla.geckoview {
@UiThread public static interface WebExtensionController.PromptDelegate {
method @Deprecated @DeprecationSchedule(id="web-extension-required-permissions",version=133) @Nullable default public GeckoResult<AllowOrDeny> onInstallPrompt(@NonNull WebExtension);
method @Nullable default public GeckoResult<AllowOrDeny> onInstallPrompt(@NonNull WebExtension, @NonNull String[], @NonNull String[]);
method @Deprecated @DeprecationSchedule(id="web-extension-on-install-prompt",version=134) @Nullable default public GeckoResult<AllowOrDeny> onInstallPrompt(@NonNull WebExtension, @NonNull String[], @NonNull String[]);
method @Nullable default public GeckoResult<WebExtension.PermissionPromptResponse> onInstallPromptRequest(@NonNull WebExtension, @NonNull String[], @NonNull String[]);
method @Nullable default public GeckoResult<AllowOrDeny> onOptionalPrompt(@NonNull WebExtension, @NonNull String[], @NonNull String[]);
method @Nullable default public GeckoResult<AllowOrDeny> onUpdatePrompt(@NonNull WebExtension, @NonNull WebExtension, @NonNull String[], @NonNull String[]);
}

View File

@ -2589,6 +2589,7 @@ class NavigationDelegateTest : BaseSessionTest() {
sessionRule.delegateUntilTestEnd(object : WebExtensionController.PromptDelegate {
@AssertCalled
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,

View File

@ -698,6 +698,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -777,7 +778,7 @@ class WebExtensionTest : BaseSessionTest() {
}
@Test
fun installWebExtension() {
fun installWebExtensionOnInstallPrompt() {
mainSession.loadUri("https://example.com")
sessionRule.waitForPageStop()
@ -787,6 +788,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
@Deprecated("Remove test when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -846,6 +848,159 @@ class WebExtensionTest : BaseSessionTest() {
assertBodyBorderEqualTo("")
}
@Test
fun installWebExtension() {
mainSession.loadUri("https://example.com")
sessionRule.waitForPageStop()
// First let's check that the color of the border is empty before loading
// the WebExtension
assertBodyBorderEqualTo("")
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
override fun onInstallPromptRequest(
extension: WebExtension,
permissions: Array<out String>,
origins: Array<out String>,
): GeckoResult<PermissionPromptResponse>? {
assertEquals(
extension.metaData.description,
"Adds a red border to all webpages matching example.com.",
)
assertEquals(extension.metaData.name, "Borderify")
assertEquals(extension.metaData.version, "1.0")
assertEquals(extension.isBuiltIn, false)
assertEquals(extension.metaData.enabled, false)
assertEquals(
extension.metaData.signedState,
WebExtension.SignedStateFlags.SIGNED,
)
assertEquals(
extension.metaData.blocklistState,
WebExtension.BlocklistStateFlags.NOT_BLOCKED,
)
assertEquals(extension.metaData.incognito, "spanning")
return GeckoResult.fromValue(
PermissionPromptResponse(
true, // isPermissionsGranted
false, // isPrivateModeGranted
),
)
}
})
val borderify = sessionRule.waitForResult(
controller.install(
"resource://android/assets/web_extensions/borderify.xpi",
null,
),
)
mainSession.reload()
sessionRule.waitForPageStop()
// Check that the WebExtension was applied by checking the border color
assertBodyBorderEqualTo("red")
assertFalse(borderify.metaData.allowedInPrivateBrowsing)
var list = extensionsMap(sessionRule.waitForResult(controller.list()))
assertEquals(list.size, 2)
assertTrue(list.containsKey(borderify.id))
assertTrue(list.containsKey(RuntimeCreator.TEST_SUPPORT_EXTENSION_ID))
// Uninstall WebExtension and check again
sessionRule.waitForResult(controller.uninstall(borderify))
list = extensionsMap(sessionRule.waitForResult(controller.list()))
assertEquals(list.size, 1)
assertTrue(list.containsKey(RuntimeCreator.TEST_SUPPORT_EXTENSION_ID))
mainSession.reload()
sessionRule.waitForPageStop()
// Check that the WebExtension was not applied after being uninstalled
assertBodyBorderEqualTo("")
}
@Test
@Setting.List(Setting(key = Setting.Key.USE_PRIVATE_MODE, value = "true"))
fun installWebExtensionAllowInPrivateMode() {
mainSession.loadUri("https://example.com")
sessionRule.waitForPageStop()
// First let's check that the color of the border is empty before loading
// the WebExtension
assertBodyBorderEqualTo("")
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
override fun onInstallPromptRequest(
extension: WebExtension,
permissions: Array<out String>,
origins: Array<out String>,
): GeckoResult<PermissionPromptResponse>? {
assertEquals(
extension.metaData.description,
"Adds a red border to all webpages matching example.com.",
)
assertEquals(extension.metaData.name, "Borderify")
assertEquals(extension.metaData.version, "1.0")
assertEquals(extension.isBuiltIn, false)
assertEquals(extension.metaData.enabled, false)
assertEquals(
extension.metaData.signedState,
WebExtension.SignedStateFlags.SIGNED,
)
assertEquals(
extension.metaData.blocklistState,
WebExtension.BlocklistStateFlags.NOT_BLOCKED,
)
assertEquals(extension.metaData.incognito, "spanning")
return GeckoResult.fromValue(
PermissionPromptResponse(
true, // isPermissionsGranted
true, // isPrivateModeGranted
),
)
}
})
val borderify = sessionRule.waitForResult(
controller.install(
"resource://android/assets/web_extensions/borderify.xpi",
null,
),
)
mainSession.reload()
sessionRule.waitForPageStop()
// Check that the WebExtension was applied by checking the border color
assertBodyBorderEqualTo("red")
assertTrue(mainSession.settings.usePrivateMode)
assertTrue(borderify.metaData.allowedInPrivateBrowsing)
var list = extensionsMap(sessionRule.waitForResult(controller.list()))
assertEquals(list.size, 2)
assertTrue(list.containsKey(borderify.id))
assertTrue(list.containsKey(RuntimeCreator.TEST_SUPPORT_EXTENSION_ID))
// Uninstall WebExtension and check again
sessionRule.waitForResult(controller.uninstall(borderify))
list = extensionsMap(sessionRule.waitForResult(controller.list()))
assertEquals(list.size, 1)
assertTrue(list.containsKey(RuntimeCreator.TEST_SUPPORT_EXTENSION_ID))
mainSession.reload()
sessionRule.waitForPageStop()
// Check that the WebExtension was not applied after being uninstalled
assertBodyBorderEqualTo("")
}
@Test
@Setting.List(Setting(key = Setting.Key.USE_PRIVATE_MODE, value = "true"))
fun runInPrivateBrowsing() {
@ -857,6 +1012,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled(count = 1)
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -940,6 +1096,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled(count = 1)
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -1015,6 +1172,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled(count = 2)
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -1070,6 +1228,7 @@ class WebExtensionTest : BaseSessionTest() {
) {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled(count = 0)
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -1130,6 +1289,7 @@ class WebExtensionTest : BaseSessionTest() {
)
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -1228,6 +1388,7 @@ class WebExtensionTest : BaseSessionTest() {
fun corruptFileErrorWillNotReturnAnWebExtensionWithoutId() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled(count = 0)
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -1292,6 +1453,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled(count = 1)
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -1902,6 +2064,7 @@ class WebExtensionTest : BaseSessionTest() {
)
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -1980,6 +2143,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -1994,6 +2158,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -2909,6 +3074,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -2970,6 +3136,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled(count = 1)
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -3011,6 +3178,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -3059,6 +3227,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -3134,6 +3303,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -3191,6 +3361,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -3267,6 +3438,7 @@ class WebExtensionTest : BaseSessionTest() {
fun cancelInstallFailsAfterInstalled() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -3308,6 +3480,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -3370,6 +3543,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -3529,6 +3703,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -3608,6 +3783,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -3901,6 +4077,7 @@ class WebExtensionTest : BaseSessionTest() {
var addonId = ""
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,
@ -4041,6 +4218,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled(count = 1)
@Deprecated("Update to the new API when addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374")
override fun onInstallPrompt(
extension: WebExtension,
permissions: Array<String>,

View File

@ -2965,4 +2965,29 @@ public class WebExtension {
this.initData = initData;
}
}
/**
* Holds all the information which the user has submited as part of a confirmation of a
* permissions prompt request.
*/
public static class PermissionPromptResponse {
/** Whether the user granted permissions or not. */
@Nullable public final Boolean isPermissionsGranted;
/** Whether the user granted access in private mode or not. */
@Nullable public final Boolean isPrivateModeGranted;
/**
* Creates a new PermissionPromptResponse with the given fields.
*
* @param isPermissionsGranted Whether the user granted permissions or not.
* @param isPrivateModeGranted Whether the user granted access in private mode or not.
*/
public PermissionPromptResponse(
final @Nullable Boolean isPermissionsGranted,
final @Nullable Boolean isPrivateModeGranted) {
this.isPermissionsGranted = isPermissionsGranted;
this.isPrivateModeGranted = isPrivateModeGranted;
}
}
}

View File

@ -266,9 +266,9 @@ public class WebExtensionController {
}
/**
* Called whenever a new extension is being installed. This is intended as an opportunity for
* the app to prompt the user for the permissions required by this extension.
*
* @deprecated Please use onInstallPromptRequest instead. Called whenever a new extension is
* being installed. This is intended as an opportunity for the app to prompt the user for
* the permissions required by this extension.
* @param extension The {@link WebExtension} that is about to be installed. You can use {@link
* WebExtension#metaData} to gather information about this extension when building the user
* prompt dialog.
@ -280,6 +280,8 @@ public class WebExtensionController {
* DENY}.
*/
@Nullable
@Deprecated
@DeprecationSchedule(id = "web-extension-on-install-prompt", version = 134)
default GeckoResult<AllowOrDeny> onInstallPrompt(
@NonNull final WebExtension extension,
@NonNull final String[] permissions,
@ -287,6 +289,26 @@ public class WebExtensionController {
return null;
}
/**
* Called whenever a new extension is being installed. This is intended as an opportunity for
* the app to prompt the user for the permissions required by this extension.
*
* @param extension The {@link WebExtension} that is about to be installed. You can use {@link
* WebExtension#metaData} to gather information about this extension when building the user
* prompt dialog.
* @param permissions The list of permissions that are granted during installation.
* @param origins The list of origins that are granted during installation.
* @return A {@link GeckoResult} that completes with a {@link
* WebExtension.PermissionPromptResponse} containing all the details from the user response.
*/
@Nullable
default GeckoResult<WebExtension.PermissionPromptResponse> onInstallPromptRequest(
@NonNull final WebExtension extension,
@NonNull final String[] permissions,
@NonNull final String[] origins) {
return null;
}
/**
* Called whenever an updated extension has new permissions. This is intended as an opportunity
* for the app to prompt the user for the new permissions required by this extension.
@ -632,7 +654,8 @@ public class WebExtensionController {
*
* <p>When calling this method, the GeckoView library will download the extension, validate its
* manifest and signature, and give you an opportunity to verify its permissions through {@link
* PromptDelegate#installPrompt}, you can use this method to prompt the user if appropriate.
* PromptDelegate#onInstallPromptRequest}, you can use this method to prompt the user if
* appropriate.
*
* @param uri URI to the extension's <code>.xpi</code> package. This can be a remote <code>https:
* </code> URI or a local <code>file:</code> or <code>resource:</code> URI. Note: the app
@ -646,7 +669,7 @@ public class WebExtensionController {
* exceptionally with a {@link WebExtension.InstallException InstallException} that will
* contain the relevant error code in {@link WebExtension.InstallException#code
* InstallException#code}.
* @see PromptDelegate#installPrompt
* @see PromptDelegate#onInstallPromptRequest(WebExtension, String[], String[])
* @see WebExtension.InstallException.ErrorCodes
* @see WebExtension#metaData
*/
@ -683,9 +706,9 @@ public class WebExtensionController {
*
* <p>When calling this method, the GeckoView library will download the extension, validate its
* manifest and signature, and give you an opportunity to verify its permissions through {@link
* PromptDelegate#installPrompt}, you can use this method to prompt the user if appropriate. If
* you are looking to provide an {@link InstallationMethod}, please use {@link
* WebExtensionController#install(String, String)}
* PromptDelegate#installPromptRequest(GeckoBundle, EventCallback)}, you can use this method to
* prompt the user if appropriate. If you are looking to provide an {@link InstallationMethod},
* please use {@link WebExtensionController#install(String, String)}
*
* @param uri URI to the extension's <code>.xpi</code> package. This can be a remote <code>https:
* </code> URI or a local <code>file:</code> or <code>resource:</code> URI. Note: the app
@ -698,7 +721,7 @@ public class WebExtensionController {
* exceptionally with a {@link WebExtension.InstallException InstallException} that will
* contain the relevant error code in {@link WebExtension.InstallException#code
* InstallException#code}.
* @see PromptDelegate#installPrompt
* @see PromptDelegate#installPromptRequest
* @see WebExtension.InstallException.ErrorCodes
* @see WebExtension#metaData
*/
@ -997,7 +1020,7 @@ public class WebExtensionController {
* @return A {@link GeckoResult} that will complete when the update process finishes. If an update
* is found and installed successfully, the GeckoResult will return the updated {@link
* WebExtension}. If no update is available, null will be returned. If the updated extension
* requires new permissions, the {@link PromptDelegate#installPrompt} will be called.
* requires new permissions, the {@link PromptDelegate#installPromptRequest} will be called.
* @see PromptDelegate#updatePrompt
*/
@AnyThread
@ -1041,7 +1064,7 @@ public class WebExtensionController {
Log.d(LOGTAG, "handleMessage " + event);
if ("GeckoView:WebExtension:InstallPrompt".equals(event)) {
installPrompt(bundle, callback);
installPromptRequest(bundle, callback);
return;
} else if ("GeckoView:WebExtension:UpdatePrompt".equals(event)) {
updatePrompt(bundle, callback);
@ -1166,7 +1189,7 @@ public class WebExtensionController {
});
}
private void installPrompt(final GeckoBundle message, final EventCallback callback) {
private void installPromptRequest(final GeckoBundle message, final EventCallback callback) {
final GeckoBundle extensionBundle = message.getBundle("extension");
if (extensionBundle == null
|| !extensionBundle.containsKey("webExtensionId")
@ -1187,20 +1210,38 @@ public class WebExtensionController {
return;
}
final GeckoResult<AllowOrDeny> promptResponse =
@SuppressWarnings("deprecation")
final GeckoResult<AllowOrDeny> promptResponseDeprecated =
mPromptDelegate.onInstallPrompt(
extension, message.getStringArray("permissions"), message.getStringArray("origins"));
if (promptResponse == null) {
return;
// To be deleted after addressing https://bugzilla.mozilla.org/show_bug.cgi?id=1919374
// If we get null from onInstallPrompt this means nobody has implemented it, so we proceed to
// call the new API onInstallPromptRequest.
if (promptResponseDeprecated != null) {
callback.resolveTo(
promptResponseDeprecated.map(
allowOrDeny -> {
final GeckoBundle response = new GeckoBundle(2);
response.putBoolean("allow", AllowOrDeny.ALLOW.equals(allowOrDeny));
response.putBoolean("privateBrowsingAllowed", false);
return response;
}));
} else {
final GeckoResult<WebExtension.PermissionPromptResponse> promptResponse =
mPromptDelegate.onInstallPromptRequest(
extension, message.getStringArray("permissions"), message.getStringArray("origins"));
if (promptResponse == null) {
return;
}
callback.resolveTo(
promptResponse.map(
userResponse -> {
final GeckoBundle response = new GeckoBundle(2);
response.putBoolean("allow", userResponse.isPermissionsGranted);
response.putBoolean("privateBrowsingAllowed", userResponse.isPrivateModeGranted);
return response;
}));
}
callback.resolveTo(
promptResponse.map(
allowOrDeny -> {
final GeckoBundle response = new GeckoBundle(1);
response.putBoolean("allow", AllowOrDeny.ALLOW.equals(allowOrDeny));
return response;
}));
}
private void updatePrompt(final GeckoBundle message, final EventCallback callback) {

View File

@ -15,10 +15,16 @@ exclude: true
## v133
- Added [`GeckoSession.getWebCompatInfo`][133.1] that returns a `GeckoResult<JSONObject>` for web compatability information. ([bug 1917273]({{bugzilla}}1917273)).
-Added [`isInteractiveWidgetDefaultResizesVisual`][133.2] to tell the preference value of "dom.interactive_widget_default_resizes_visual".
-Added [`isInteractiveWidgetDefaultResizesVisual`][133.2] to tell the preference value of "dom.interactive_widget_default_resizes_visual".
- Added [`WebExtension.PermissionPromptResponse`][133.3] Represents a response from `WebExtension` prompt request.
- Added [`WebExtension.onInstallPromptRequest`][133.4] Delegate notified when install prompt needs to be shown.
- ⚠️ [`WebExtensionController.PromptDelegate.onInstallPrompt`][133.5] is deprecated, and it will be deleted in version 134 see https://bugzilla.mozilla.org/show_bug.cgi?id=1919374.
[133.1]: {{javadoc_uri}}/GeckoSession.html#getWebCompatInfo()
[133.2]: {{javadoc_uri}}/GeckoRuntime.html#isInteractiveWidgetDefaultResizesVisual()
[133.3]: {{javadoc_uri}}/WebExtension.PermissionPromptResponse.html
[133.4]: {{javadoc_uri}}/WebExtensionController.PromptDelegate.html#onInstallPromptRequest
[133.5]: {{javadoc_uri}}/WebExtensionController.PromptDelegate.html#onInstallPrompt
## v132
-Added [`getDisableShip`][132.1] to get the setting for Session History in Parent (SHIP)) and [`disableShip`][132.2] to set the status of SHIP on the `GeckoRuntimeSettings` builder.
@ -1622,4 +1628,4 @@ to allow adding gecko profiler markers.
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport(android.content.Context,android.os.Bundle,java.lang.String)
[65.25]: {{javadoc_uri}}/GeckoResult.html
[api-version]: 39fcf644fe97ddf83f54acd333e98062d1d1f0e7
[api-version]: 926fb0231b94aa42486a1084d258e45e3d20bcab

View File

@ -143,11 +143,13 @@ class WebExtensionManager
@Nullable
@Override
public GeckoResult<AllowOrDeny> onInstallPrompt(
final @NonNull WebExtension extension,
@NonNull String[] permissions,
@NonNull String[] origins) {
return GeckoResult.allow();
public GeckoResult<WebExtension.PermissionPromptResponse> onInstallPromptRequest(
@NonNull WebExtension extension, @NonNull String[] permissions, @NonNull String[] origins) {
return GeckoResult.fromValue(
new org.mozilla.geckoview.WebExtension.PermissionPromptResponse(
true, // isPermissionsGranted
true // isPrivateModeGranted
));
}
@Nullable

View File

@ -5,8 +5,9 @@
import { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs";
import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
const PRIVATE_BROWSING_PERMISSION = {
permissions: ["internal:privateBrowsingAllowed"],
const PRIVATE_BROWSING_PERM_NAME = "internal:privateBrowsingAllowed";
const PRIVATE_BROWSING_PERMS = {
permissions: [PRIVATE_BROWSING_PERM_NAME],
origins: [],
};
@ -354,7 +355,13 @@ async function exportExtension(aAddon, aSourceURI) {
disabledFlags.push("appVersionDisabled");
}
const baseURL = policy ? policy.getURL() : "";
const privateBrowsingAllowed = policy ? policy.privateBrowsingAllowed : false;
let privateBrowsingAllowed;
if (policy) {
privateBrowsingAllowed = policy.privateBrowsingAllowed;
} else {
const { permissions } = await lazy.ExtensionPermissions.get(aAddon.id);
privateBrowsingAllowed = permissions.includes(PRIVATE_BROWSING_PERM_NAME);
}
let updateDate;
try {
@ -520,7 +527,7 @@ class ExtensionPromptObserver {
Services.obs.addObserver(this, "webextension-optional-permission-prompt");
}
async permissionPrompt(aInstall, aAddon, aInfo) {
async permissionPromptRequest(aInstall, aAddon, aInfo) {
const { sourceURI } = aInstall;
const { permissions } = aInfo;
@ -533,6 +540,14 @@ class ExtensionPromptObserver {
});
if (response.allow) {
if (response.privateBrowsingAllowed) {
await lazy.ExtensionPermissions.add(aAddon.id, PRIVATE_BROWSING_PERMS);
} else {
await lazy.ExtensionPermissions.remove(
aAddon.id,
PRIVATE_BROWSING_PERMS
);
}
aInfo.resolve();
} else {
aInfo.reject();
@ -555,7 +570,7 @@ class ExtensionPromptObserver {
case "webextension-permission-prompt": {
const { info } = aSubject.wrappedJSObject;
const { addon, install } = info;
this.permissionPrompt(install, addon, info);
this.permissionPromptRequest(install, addon, info);
break;
}
case "webextension-optional-permission-prompt": {
@ -960,9 +975,9 @@ export var GeckoViewWebExtension = {
async setPrivateBrowsingAllowed(aId, aAllowed) {
if (aAllowed) {
await lazy.ExtensionPermissions.add(aId, PRIVATE_BROWSING_PERMISSION);
await lazy.ExtensionPermissions.add(aId, PRIVATE_BROWSING_PERMS);
} else {
await lazy.ExtensionPermissions.remove(aId, PRIVATE_BROWSING_PERMISSION);
await lazy.ExtensionPermissions.remove(aId, PRIVATE_BROWSING_PERMS);
}
// Reload the extension if it is already enabled. This ensures any change