Commit f5e5b985 authored by Zac McKenney's avatar Zac McKenney
Browse files

Bug 1811563 - Redesign permissions screen for addons r=android-reviewers,amejiamarmol,gl

parent e5427c90
Loading
Loading
Loading
Loading
+69 −0
Original line number Diff line number Diff line
@@ -125,6 +125,19 @@ data class Addon(
        val granted: Boolean,
    ) : Parcelable

    /**
     * Localized permission from [Permission]
     *
     * @property localizedName The localized name of the permission to show in the UI.
     * @property permission The [Permission] that was localized.
     */
    @SuppressLint("ParcelCreator")
    @Parcelize
    data class LocalizedPermission(
        val localizedName: String,
        val permission: Permission,
    ) : Parcelable

    /**
     * Returns a list of id resources per each item on the [Addon.permissions] list.
     * Holds the state of the installed web extension of this add-on.
@@ -215,6 +228,14 @@ data class Addon(
        return localizePermissions(permissions, context)
    }

    /**
     * Returns a [LocalizedPermission] list of the optional permissions.
     * @param context Context for resource lookup
     */
    fun translateOptionalPermissions(context: Context): List<LocalizedPermission> {
        return localizeOptionalPermissions(optionalPermissions, context)
    }

    /**
     * Returns whether or not this [Addon] is currently installed.
     */
@@ -333,6 +354,49 @@ data class Addon(
            return localizedNormalPermissions + localizedUrlAccessPermissions
        }

        /**
         * Takes a list of optional permissions and returns the list of [LocalizedPermission].
         *
         * @param optionalPermissions The list of optional permissions
         * @param context The context for resource lookup
         */
        fun localizeOptionalPermissions(
            optionalPermissions: List<Permission>,
            context: Context,
        ): List<LocalizedPermission> {
            var allUrlAccessPermissionFound = false
            val notFoundPermissions = mutableListOf<Permission>()
            val localizedURLAccessPermissions = mutableListOf<LocalizedPermission>()

            val localizedOptionalPermissions: List<LocalizedPermission> = optionalPermissions.mapNotNull {
                val resourceId = permissionToTranslation[it.name]
                if (resourceId != null) {
                    if (resourceId.isAllURLsPermission()) allUrlAccessPermissionFound = true
                    LocalizedPermission(context.getString(resourceId), it)
                } else {
                    notFoundPermissions.add(it)
                    null
                }
            }

            if (!allUrlAccessPermissionFound && notFoundPermissions.isNotEmpty()) {
                notFoundPermissions.mapNotNullTo(localizedURLAccessPermissions) { permission ->
                    when (val localizedResourceId = localizeURLAccessPermission(permission.name)) {
                        null -> {
                            // Hide if we can't find a string resource to localize the permission
                            null
                        }
                        else -> {
                            val localizedName = context.getString(localizedResourceId)
                            LocalizedPermission(localizedName, permission)
                        }
                    }
                }
            }

            return localizedOptionalPermissions + localizedURLAccessPermissions
        }

        /**
         * Creates an [Addon] object from a [WebExtension] one. The resulting object might have an installed state when
         * the second method's argument is used.
@@ -517,6 +581,11 @@ data class Addon(
            return this == R.string.mozac_feature_addons_permissions_all_urls_description
        }

        fun Permission.isAllURLsPermission(): Boolean {
            return permissionToTranslation[name]?.isAllURLsPermission()
                ?: (localizeURLAccessPermission(name)?.isAllURLsPermission() == true)
        }

        internal fun localizeURLAccessPermission(urlAccess: String): Int? {
            val uri = urlAccess.toUri()
            val host = (uri.host ?: "").trim()
+22 −0
Original line number Diff line number Diff line
@@ -9,6 +9,7 @@ import mozilla.components.concept.engine.webextension.DisabledFlags
import mozilla.components.concept.engine.webextension.Incognito
import mozilla.components.concept.engine.webextension.Metadata
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.feature.addons.Addon.Companion.localizeOptionalPermissions
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.whenever
@@ -350,6 +351,27 @@ class AddonTest {
        }
    }

    @Test
    fun `localizeOptionalPermissions - should provide LocalizedPermission list`() {
        val expectedLocalizedPermissions = listOf(
            Addon.LocalizedPermission(testContext.getString(R.string.mozac_feature_addons_permissions_all_urls_description), Addon.Permission("<all_urls>", false)),
            Addon.LocalizedPermission(testContext.getString(R.string.mozac_feature_addons_permissions_web_navigation_description), Addon.Permission("webNavigation", false)),
            Addon.LocalizedPermission(testContext.getString(R.string.mozac_feature_addons_permissions_clipboard_read_description), Addon.Permission("clipboardRead", false)),
            Addon.LocalizedPermission(testContext.getString(R.string.mozac_feature_addons_permissions_clipboard_write_description), Addon.Permission("clipboardWrite", false)),
        )

        val permissions = listOf(
            Addon.Permission("<all_urls>", false),
            Addon.Permission("webNavigation", false),
            Addon.Permission("clipboardRead", false),
            Addon.Permission("clipboardWrite", false),
        )

        val localizedResult = localizeOptionalPermissions(permissions, testContext)

        assertEquals(expectedLocalizedPermissions, localizedResult)
    }

    @Test
    fun `newFromWebExtension - must return an Addon instance`() {
        val version = "1.2.3"
+0 −60
Original line number Diff line number Diff line
/* 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 org.mozilla.fenix.addons

import android.net.Uri
import androidx.core.net.toUri
import androidx.recyclerview.widget.LinearLayoutManager
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.AddonPermissionsAdapter
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.FragmentAddOnPermissionsBinding
import org.mozilla.fenix.theme.ThemeManager

interface AddonPermissionsDetailsInteractor {

    /**
     * Open the given siteUrl in the browser.
     */
    fun openWebsite(addonSiteUrl: Uri)
}

/**
 * Shows the permission details of an add-on.
 */
class AddonPermissionDetailsBindingDelegate(
    val binding: FragmentAddOnPermissionsBinding,
    private val interactor: AddonPermissionsDetailsInteractor,
) {

    fun bind(addon: Addon) {
        bindPermissions(addon)
        bindLearnMore()
    }

    private fun bindPermissions(addon: Addon) {
        binding.addOnsPermissions.apply {
            layoutManager = LinearLayoutManager(context)
            val sortedPermissions = addon.translatePermissions(context).sorted()
            adapter = AddonPermissionsAdapter(
                sortedPermissions,
                style = AddonPermissionsAdapter.Style(
                    ThemeManager.resolveAttribute(R.attr.textPrimary, context),
                ),
            )
        }
    }

    private fun bindLearnMore() {
        binding.learnMoreLabel.setOnClickListener {
            interactor.openWebsite(LEARN_MORE_URL.toUri())
        }
    }

    private companion object {
        const val LEARN_MORE_URL =
            "https://support.mozilla.org/kb/permission-request-messages-firefox-extensions"
    }
}
+145 −18
Original line number Diff line number Diff line
@@ -4,45 +4,172 @@

package org.mozilla.fenix.addons

import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.navArgs
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.Addon.Companion.isAllURLsPermission
import mozilla.components.feature.addons.ui.translateName
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.FragmentAddOnPermissionsBinding
import org.mozilla.fenix.addons.ui.AddonPermissionsScreen
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.theme.FirefoxTheme

data class AddonPermissionsUpdateRequest(
    val optionalPermissions: List<String> = emptyList(),
    val originPermissions: List<String> = emptyList(),
)

/**
 * A fragment to show the permissions of an add-on.
 * A fragment to show and allow a user to change permissions for an addon.
 */
class AddonPermissionsDetailsFragment :
    Fragment(R.layout.fragment_add_on_permissions),
    AddonPermissionsDetailsInteractor {
class AddonPermissionsDetailsFragment : Fragment() {

    private val args by navArgs<AddonPermissionsDetailsFragmentArgs>()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val binding = FragmentAddOnPermissionsBinding.bind(view)
        AddonPermissionDetailsBindingDelegate(binding, interactor = this).bind(args.addon)
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View = ComposeView(requireContext()).apply {
        setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)

        setContent {
            val optionalPermissions = rememberSaveable {
                mutableStateOf(args.addon.translateOptionalPermissions(requireContext()))
            }

    override fun onResume() {
        super.onResume()
        context?.let {
            showToolbar(title = args.addon.translateName(it))
            val originPermissions = rememberSaveable {
                mutableStateOf(
                    args.addon.optionalOrigins
                        .map {
                            Addon.LocalizedPermission(it.name, it)
                        },
                )
            }

            // Note: Even if <all_urls> is in optionalPermissions of the extension manifest, it is found in
            // originPermissions of the Addon
            val allSitesHostPermissionsList = rememberSaveable {
                mutableStateOf(
                    args.addon.optionalOrigins.getAllSitesPermissionsList(),
                )
            }

    override fun openWebsite(addonSiteUrl: Uri) {
            // Update all of the mutable states when an addon is returned from updating permissions
            val onUpdatePermissionsSuccess: (Addon) -> Unit = { updatedAddon ->
                optionalPermissions.value =
                    updatedAddon.translateOptionalPermissions(requireContext())
                originPermissions.value = updatedAddon.optionalOrigins.map {
                    Addon.LocalizedPermission(it.name, it)
                }
                allSitesHostPermissionsList.value =
                    updatedAddon.optionalOrigins.getAllSitesPermissionsList()
            }

            FirefoxTheme {
                AddonPermissionsScreen(
                    permissions = args.addon.translatePermissions(requireContext()),
                    optionalPermissions = optionalPermissions.value,
                    originPermissions = originPermissions.value,
                    isAllSitesSwitchVisible = allSitesHostPermissionsList.value.isNotEmpty(),
                    isAllSitesEnabled = allSitesHostPermissionsList.value.getOrNull(0)?.granted
                        ?: false,
                    onAddOptionalPermissions = { permissionRequest ->
                        addOptionalPermissions(permissionRequest, onUpdatePermissionsSuccess)
                    },
                    onRemoveOptionalPermissions = { permissionRequest ->
                        removeOptionalPermission(permissionRequest, onUpdatePermissionsSuccess)
                    },
                    onAddAllSitesPermissions = {
                        addOptionalPermissions(
                            AddonPermissionsUpdateRequest(
                                originPermissions = allSitesHostPermissionsList.value.map { it.name },
                            ),
                            onUpdatePermissionsSuccess,
                        )
                    },
                    onRemoveAllSitesPermissions = {
                        removeOptionalPermission(
                            AddonPermissionsUpdateRequest(
                                originPermissions = allSitesHostPermissionsList.value.map { it.name },
                            ),
                            onUpdatePermissionsSuccess,
                        )
                    },
                    onLearnMoreClick = { learnMoreUrl ->
                        openWebsite(learnMoreUrl)
                    },
                )
            }
        }
    }

    private fun addOptionalPermissions(
        addPermissionsRequest: AddonPermissionsUpdateRequest,
        onUpdatePermissionsSuccess: (Addon) -> Unit,
    ) {
        requireContext().components.addonManager.addOptionalPermission(
            args.addon,
            addPermissionsRequest.optionalPermissions,
            addPermissionsRequest.originPermissions,
            onSuccess = {
                onUpdatePermissionsSuccess(it)
            },
            onError = {
                /** No-Op **/
            },
        )
    }

    private fun removeOptionalPermission(
        removePermissionsRequest: AddonPermissionsUpdateRequest,
        onUpdatePermissionsSuccess: (Addon) -> Unit,
    ) {
        requireContext().components.addonManager.removeOptionalPermission(
            args.addon,
            removePermissionsRequest.optionalPermissions,
            removePermissionsRequest.originPermissions,
            onSuccess = {
                onUpdatePermissionsSuccess(it)
            },
            onError = {
                /** No-Op **/
            },
        )
    }

    private fun openWebsite(addonSiteUrl: String) {
        (activity as HomeActivity).openToBrowserAndLoad(
            searchTermOrURL = addonSiteUrl.toString(),
            searchTermOrURL = addonSiteUrl,
            newTab = true,
            from = BrowserDirection.FromAddonPermissionsDetailsFragment,
        )
    }

    private fun <T : List<Addon.Permission>> T.getAllSitesPermissionsList(): List<Addon.Permission> {
        return this.mapNotNull {
            if (it.isAllURLsPermission()) {
                it
            } else {
                null
            }
        }
    }

    override fun onResume() {
        super.onResume()
        context?.let {
            showToolbar(title = args.addon.translateName(it))
        }
    }
}
+300 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading