diff --git a/app/src/main/java/org/mozilla/fenix/extension/WebExtensionPromptFeature.kt b/app/src/main/java/org/mozilla/fenix/extension/WebExtensionPromptFeature.kt index d9a38488f..a336041ed 100644 --- a/app/src/main/java/org/mozilla/fenix/extension/WebExtensionPromptFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/extension/WebExtensionPromptFeature.kt @@ -81,6 +81,10 @@ class WebExtensionPromptFeature( addon, promptRequest, ) + is WebExtensionPromptRequest.AfterInstallation.OptionalPermissions -> handleOptionalPermissionsRequest( + addon, + promptRequest, + ) is WebExtensionPromptRequest.AfterInstallation.PostInstallation -> handlePostInstallationRequest( addon.copy(installedState = promptRequest.extension.toInstalledState()), ) @@ -108,10 +112,32 @@ class WebExtensionPromptFeature( addon: Addon, promptRequest: WebExtensionPromptRequest.AfterInstallation.Permissions, ) { - if (hasExistingPermissionDialogFragment()) return showPermissionDialog( - addon, - promptRequest, + addon = addon, + onConfirm = promptRequest.onConfirm, + ) + } + + @VisibleForTesting + internal fun handleOptionalPermissionsRequest( + addon: Addon, + promptRequest: WebExtensionPromptRequest.AfterInstallation.OptionalPermissions, + ) { + val shouldGrantWithoutPrompt = Addon.localizePermissions(promptRequest.permissions, context).isEmpty() + + // If we don't have any promptable permissions, just proceed. + if (shouldGrantWithoutPrompt) { + promptRequest.onConfirm(true) + consumePromptRequest() + return + } + + showPermissionDialog( + // This is a bit of a hack so that the permission prompt only lists + // the optional permissions that are requested. + addon = addon.copy(permissions = promptRequest.permissions), + onConfirm = promptRequest.onConfirm, + forOptionalPermissions = true, ) } @@ -185,37 +211,43 @@ class WebExtensionPromptFeature( @VisibleForTesting internal fun showPermissionDialog( addon: Addon, - promptRequest: WebExtensionPromptRequest.AfterInstallation.Permissions, + onConfirm: (Boolean) -> Unit, + forOptionalPermissions: Boolean = false, ) { - if (!isInstallationInProgress && !hasExistingPermissionDialogFragment()) { - val dialog = PermissionsDialogFragment.newInstance( - addon = addon, - promptsStyling = PermissionsDialogFragment.PromptsStyling( - gravity = Gravity.BOTTOM, - shouldWidthMatchParent = true, - positiveButtonBackgroundColor = ThemeManager.resolveAttribute( - R.attr.accent, - context, - ), - positiveButtonTextColor = ThemeManager.resolveAttribute( - R.attr.textOnColorPrimary, - context, - ), - positiveButtonRadius = - (context.resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat(), - ), - onPositiveButtonClicked = { - handleApprovedPermissions(promptRequest) - }, - onNegativeButtonClicked = { - handleDeniedPermissions(promptRequest) - }, - ) - dialog.show( - fragmentManager, - PERMISSIONS_DIALOG_FRAGMENT_TAG, - ) + if (isInstallationInProgress || hasExistingPermissionDialogFragment()) { + return } + + val dialog = PermissionsDialogFragment.newInstance( + addon = addon, + forOptionalPermissions = forOptionalPermissions, + promptsStyling = PermissionsDialogFragment.PromptsStyling( + gravity = Gravity.BOTTOM, + shouldWidthMatchParent = true, + positiveButtonBackgroundColor = ThemeManager.resolveAttribute( + R.attr.accent, + context, + ), + positiveButtonTextColor = ThemeManager.resolveAttribute( + R.attr.textOnColorPrimary, + context, + ), + positiveButtonRadius = + (context.resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat(), + ), + onPositiveButtonClicked = { + onConfirm(true) + consumePromptRequest() + }, + onNegativeButtonClicked = { + onConfirm(false) + consumePromptRequest() + }, + ) + dialog.show( + fragmentManager, + PERMISSIONS_DIALOG_FRAGMENT_TAG, + ) } private fun tryToReAttachButtonHandlersToPreviousDialog() { diff --git a/app/src/test/java/org/mozilla/fenix/extension/WebExtensionPromptFeatureTest.kt b/app/src/test/java/org/mozilla/fenix/extension/WebExtensionPromptFeatureTest.kt index 8370922f9..c2d0dfb68 100644 --- a/app/src/test/java/org/mozilla/fenix/extension/WebExtensionPromptFeatureTest.kt +++ b/app/src/test/java/org/mozilla/fenix/extension/WebExtensionPromptFeatureTest.kt @@ -14,7 +14,9 @@ import mozilla.components.browser.state.action.WebExtensionAction.UpdatePromptRe import mozilla.components.browser.state.state.extension.WebExtensionPromptRequest import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.webextension.WebExtensionInstallException +import mozilla.components.feature.addons.Addon import mozilla.components.support.ktx.android.content.appVersionName +import mozilla.components.support.test.any import mozilla.components.support.test.ext.joinBlocking import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.rule.MainCoroutineRule @@ -216,4 +218,77 @@ class WebExtensionPromptFeatureTest { verify { webExtensionPromptFeature.showDialog(expectedTitle, expectedMessage) } } + + @Test + fun `WHEN OptionalPermissions is dispatched THEN handleOptionalPermissionsRequest is called`() { + webExtensionPromptFeature.start() + + every { webExtensionPromptFeature.handleOptionalPermissionsRequest(any(), any()) } just runs + + store.dispatch( + UpdatePromptRequestWebExtensionAction( + WebExtensionPromptRequest.AfterInstallation.OptionalPermissions( + mockk(relaxed = true), + mockk(), + mockk(), + ), + ), + ).joinBlocking() + + verify { webExtensionPromptFeature.handleOptionalPermissionsRequest(any(), any()) } + } + + @Test + fun `WHEN calling handleOptionalPermissionsRequest with permissions THEN call showPermissionDialog`() { + val addon: Addon = mockk(relaxed = true) + val onConfirm: ((Boolean) -> Unit) = mockk() + val promptRequest = WebExtensionPromptRequest.AfterInstallation.OptionalPermissions( + extension = mockk(), + permissions = listOf("tabs"), + onConfirm = onConfirm, + ) + + webExtensionPromptFeature.handleOptionalPermissionsRequest( + addon = addon, + promptRequest = promptRequest, + ) + + verify { webExtensionPromptFeature.showPermissionDialog(any(), eq(onConfirm), eq(true)) } + // We should verify that only the requested optional permissions are assigned to the add-on because + // those are the permissions that are going to be listed in the dialog. + verify { addon.copy(permissions = promptRequest.permissions) } + } + + @Test + fun `WHEN calling handleOptionalPermissionsRequest with a permission that doesn't have a description THEN do not call showPermissionDialog`() { + val onConfirm: ((Boolean) -> Unit) = mockk() + every { onConfirm(any()) } just runs + val promptRequest = WebExtensionPromptRequest.AfterInstallation.OptionalPermissions( + extension = mockk(), + // The "scripting" API permission doesn't have a description so we should not show a dialog for it. + permissions = listOf("scripting"), + onConfirm = onConfirm, + ) + + webExtensionPromptFeature.handleOptionalPermissionsRequest(addon = mockk(relaxed = true), promptRequest = promptRequest) + + verify(exactly = 0) { webExtensionPromptFeature.showPermissionDialog(any(), any(), any()) } + verify(exactly = 1) { onConfirm(true) } + } + + @Test + fun `WHEN calling handleOptionalPermissionsRequest with no permissions THEN do not call showPermissionDialog`() { + val onConfirm: ((Boolean) -> Unit) = mockk() + every { onConfirm(any()) } just runs + val promptRequest = WebExtensionPromptRequest.AfterInstallation.OptionalPermissions( + extension = mockk(), + permissions = emptyList(), + onConfirm = onConfirm, + ) + + webExtensionPromptFeature.handleOptionalPermissionsRequest(addon = mockk(relaxed = true), promptRequest = promptRequest) + + verify(exactly = 0) { webExtensionPromptFeature.showPermissionDialog(any(), any(), any()) } + verify(exactly = 1) { onConfirm(true) } + } }