Commit c81d2b28 authored by MozLando's avatar MozLando
Browse files

Merge #5110

5110: Closes #5036: Add dialog for accepting addon permissions r=csadilek a=Amejia481

[Video demo ](https://drive.google.com/file/d/1xyLa539tuQD1hF078QMfV2dSNcSzq_8W/view?usp=sharing

)


Co-authored-by: default avatarArturo Mejia <arturomejiamarmol@gmail.com>
parents b5c787f3 3f1d5d28
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ android {


dependencies {
    implementation Dependencies.androidx_appcompat
    implementation Dependencies.kotlin_stdlib
    implementation Dependencies.kotlin_coroutines

+52 −2
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@ package org.mozilla.samples.browser.addons

import android.content.Intent
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -22,6 +23,7 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import mozilla.components.feature.addons.AddOn
import org.mozilla.samples.browser.R
import org.mozilla.samples.browser.addons.PermissionsDialogFragment.PromptsStyling
import org.mozilla.samples.browser.ext.components

/**
@@ -44,6 +46,14 @@ class AddOnsFragment : Fragment(), View.OnClickListener {
        bindRecyclerView(rootView)
    }

    override fun onStart() {
        super.onStart()
        findPreviousDialogFragment()?.let { dialog ->
            dialog.onPositiveButtonClicked = onPositiveButtonClicked
            dialog.onNegativeButtonClicked = onNegativeButtonClicked
        }
    }

    private fun bindRecyclerView(rootView: View) {
        recyclerView = rootView.findViewById(R.id.add_ons_list)
        recyclerView.layoutManager = LinearLayoutManager(requireContext())
@@ -140,8 +150,8 @@ class AddOnsFragment : Fragment(), View.OnClickListener {
        val context = view.context
        when (view.id) {
            R.id.add_button -> {
                Toast.makeText(this.requireContext(), "Installing add-on", Toast.LENGTH_SHORT)
                    .show()
                val addOn = (((view.parent) as View).tag as AddOn)
                showPermissionDialog(addOn)
            }
            R.id.add_on_item -> {
                val intent = Intent(context, InstalledAddOnDetailsActivity::class.java)
@@ -152,4 +162,44 @@ class AddOnsFragment : Fragment(), View.OnClickListener {
            }
        }
    }

    private fun isAlreadyADialogCreated(): Boolean {
        return findPreviousDialogFragment() != null
    }

    private fun findPreviousDialogFragment(): PermissionsDialogFragment? {
        return fragmentManager?.findFragmentByTag(PERMISSIONS_DIALOG_FRAGMENT_TAG) as? PermissionsDialogFragment
    }

    private fun showPermissionDialog(addOn: AddOn) {

        val dialog = PermissionsDialogFragment.newInstance(
            addOnId = addOn.id,
            title = addOn.translatableName.translate(),
            permissions = addOn.translatePermissions(),
            promptsStyling = PromptsStyling(
                gravity = Gravity.BOTTOM,
                shouldWidthMatchParent = true
            ),
            onPositiveButtonClicked = onPositiveButtonClicked,
            onNegativeButtonClicked = onNegativeButtonClicked
        )

        if (!isAlreadyADialogCreated() && fragmentManager != null) {
            dialog.show(requireFragmentManager(), PERMISSIONS_DIALOG_FRAGMENT_TAG)
        }
    }

    private val onPositiveButtonClicked: ((String) -> Unit) = { addonId ->
        Toast.makeText(this.requireContext(), "Installing add-on $addonId", Toast.LENGTH_SHORT)
            .show()
    }

    private val onNegativeButtonClicked = {
        Toast.makeText(this.requireContext(), "Cancel button clicked", Toast.LENGTH_SHORT)
            .show()
    }
    companion object {
        private const val PERMISSIONS_DIALOG_FRAGMENT_TAG = "ADDONS_PERMISSIONS_DIALOG_FRAGMENT"
    }
}
+231 −0
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.samples.browser.addons

import android.annotation.SuppressLint
import android.app.Dialog
import android.content.DialogInterface
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.Window
import android.widget.TextView
import android.view.ViewGroup
import android.widget.Button
import android.widget.LinearLayout.LayoutParams
import androidx.annotation.ColorRes
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.core.content.ContextCompat
import org.mozilla.samples.browser.R

internal const val KEY_ADD_ON_ID = "KEY_ADD_ON_ID"
internal const val KEY_TITLE = "KEY_TITLE"
private const val KEY_DIALOG_GRAVITY = "KEY_DIALOG_GRAVITY"
private const val KEY_DIALOG_WIDTH_MATCH_PARENT = "KEY_DIALOG_WIDTH_MATCH_PARENT"
private const val KEY_PERMISSIONS = "KEY_PERMISSIONS"
private const val KEY_POSITIVE_BUTTON_BACKGROUND_COLOR = "KEY_POSITIVE_BUTTON_BACKGROUND_COLOR"
private const val KEY_POSITIVE_BUTTON_TEXT_COLOR = "KEY_POSITIVE_BUTTON_TEXT_COLOR"
private const val KEY_POSITIVE_BUTTON_RADIUS = "KEY_POSITIVE_BUTTON_RADIUS"
private const val DEFAULT_VALUE = Int.MAX_VALUE

internal class PermissionsDialogFragment : AppCompatDialogFragment() {

    internal var onPositiveButtonClicked: ((String) -> Unit)? = null
    internal var onNegativeButtonClicked: (() -> Unit)? = null

    private val safeArguments get() = requireNotNull(arguments)

    internal val addOnId get() = safeArguments.getString(KEY_ADD_ON_ID, "")

    internal val positiveButtonRadius
        get() =
            safeArguments.getFloat(KEY_POSITIVE_BUTTON_RADIUS, DEFAULT_VALUE.toFloat())

    internal val permissions: IntArray
        get() =
            safeArguments.getIntArray(KEY_PERMISSIONS) ?: intArrayOf()

    internal val title: String
        get() =
            safeArguments.getString(KEY_TITLE, "")

    internal val dialogGravity: Int
        get() =
            safeArguments.getInt(KEY_DIALOG_GRAVITY, DEFAULT_VALUE)
    internal val dialogShouldWidthMatchParent: Boolean
        get() =
            safeArguments.getBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT)

    internal val positiveButtonBackgroundColor
        get() =
            safeArguments.getInt(KEY_POSITIVE_BUTTON_BACKGROUND_COLOR, DEFAULT_VALUE)

    internal val positiveButtonTextColor
        get() =
            safeArguments.getInt(KEY_POSITIVE_BUTTON_TEXT_COLOR, DEFAULT_VALUE)

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val sheetDialog = Dialog(requireContext())
        sheetDialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
        sheetDialog.setCanceledOnTouchOutside(true)

        val rootView = createContainer()

        sheetDialog.setContainerView(rootView)

        sheetDialog.window?.apply {

            if (dialogGravity != DEFAULT_VALUE) {
                setGravity(dialogGravity)
            }

            if (dialogShouldWidthMatchParent) {
                setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
                // This must be called after addContentView, or it won't fully fill to the edge.
                setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
            }
        }

        return sheetDialog
    }

    override fun onDismiss(dialog: DialogInterface) {
        super.onDismiss(dialog)
        onNegativeButtonClicked?.invoke()
    }

    private fun Dialog.setContainerView(rootView: View) {
        if (dialogShouldWidthMatchParent) {
            setContentView(rootView)
        } else {
            addContentView(
                rootView,
                LayoutParams(
                    LayoutParams.MATCH_PARENT,
                    LayoutParams.MATCH_PARENT
                )
            )
        }
    }

    @SuppressLint("InflateParams")
    private fun createContainer(): View {
        val rootView = LayoutInflater.from(requireContext()).inflate(
            R.layout.fragment_dialog_addon_permissions,
            null,
            false
        )
        rootView.findViewById<TextView>(R.id.title).text =
            requireContext().getString(R.string.addon_permissions_dialog_title, title)
        rootView.findViewById<TextView>(R.id.permissions).text = buildPermissionsText()

        val positiveButton = rootView.findViewById<Button>(R.id.allow_button)
        val negativeButton = rootView.findViewById<Button>(R.id.deny_button)

        positiveButton.setOnClickListener {
            onPositiveButtonClicked?.invoke(addOnId)
            dismiss()
        }

        if (positiveButtonBackgroundColor != DEFAULT_VALUE) {
            val backgroundTintList =
                ContextCompat.getColorStateList(requireContext(), positiveButtonBackgroundColor)
            positiveButton.backgroundTintList = backgroundTintList
        }

        if (positiveButtonTextColor != DEFAULT_VALUE) {
            val color = ContextCompat.getColor(requireContext(), positiveButtonTextColor)
            positiveButton.setTextColor(color)
        }

        if (positiveButtonRadius != DEFAULT_VALUE.toFloat()) {
            val shape = GradientDrawable()
            shape.shape = GradientDrawable.RECTANGLE
            shape.setColor(
                ContextCompat.getColor(
                    requireContext(),
                    positiveButtonBackgroundColor
                )
            )
            shape.cornerRadius = positiveButtonRadius
            positiveButton.background = shape
        }

        negativeButton.setOnClickListener {
            onNegativeButtonClicked?.invoke()
            dismiss()
        }

        return rootView
    }

    private fun buildPermissionsText(): String {
        var permissionsText = getString(R.string.addon_permissions_dialog_subtitle) + "\n\n"

        permissions.forEachIndexed { index, item ->
            val brakeLine = if (index + 1 != permissions.size) "\n\n" else ""
            val permissionText = requireContext().getString(item)
            permissionsText += "• $permissionText $brakeLine"
        }
        return permissionsText
    }

    @Suppress("LongParameterList")
    companion object {
        fun newInstance(
            addOnId: String,
            title: String,
            permissions: List<Int>,
            promptsStyling: PromptsStyling? = null,
            onPositiveButtonClicked: ((String) -> Unit)? = null,
            onNegativeButtonClicked: (() -> Unit)? = null
        ): PermissionsDialogFragment {

            val fragment = PermissionsDialogFragment()
            val arguments = fragment.arguments ?: Bundle()

            arguments.apply {
                putString(KEY_ADD_ON_ID, addOnId)
                putString(KEY_TITLE, title)
                putIntArray(KEY_PERMISSIONS, permissions.toIntArray())

                promptsStyling?.gravity?.apply {
                    putInt(KEY_DIALOG_GRAVITY, this)
                }
                promptsStyling?.shouldWidthMatchParent?.apply {
                    putBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT, this)
                }
                promptsStyling?.positiveButtonBackgroundColor?.apply {
                    putInt(KEY_POSITIVE_BUTTON_BACKGROUND_COLOR, this)
                }

                promptsStyling?.positiveButtonTextColor?.apply {
                    putInt(KEY_POSITIVE_BUTTON_TEXT_COLOR, this)
                }
                println(permissions)
            }
            fragment.onPositiveButtonClicked = onPositiveButtonClicked
            fragment.onNegativeButtonClicked = onNegativeButtonClicked
            fragment.arguments = arguments
            return fragment
        }
    }

    /**
     * Styling for the permissions dialog.
     */
    data class PromptsStyling(
        val gravity: Int,
        val shouldWidthMatchParent: Boolean = false,
        @ColorRes
        val positiveButtonBackgroundColor: Int? = null,
        @ColorRes
        val positiveButtonTextColor: Int? = null,
        val positiveButtonRadius: Float? = null
    )
}
+79 −0
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/. -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?android:windowBackground"
        android:orientation="vertical"
        tools:ignore="Overdraw">

    <ImageView
            android:id="@+id/icon"
            android:layout_width="32dp"
            android:layout_height="32dp"
            android:layout_alignParentTop="true"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:importantForAccessibility="no"
            android:scaleType="center"
            android:src="@drawable/mozac_ic_extensions"
            android:tint="?android:attr/textColorPrimary" />

    <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignBaseline="@id/icon"
            android:layout_alignParentTop="true"
            android:layout_marginStart="3dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="11dp"
            android:layout_toEndOf="@id/icon"
            android:paddingStart="5dp"
            android:paddingTop="4dp"
            android:paddingEnd="5dp"
            android:textColor="?android:attr/textColorPrimary"
            android:textSize="16sp"
            tools:text="Add Ublock Origin?"
            tools:textColor="#000000" />

    <TextView
            android:id="@+id/permissions"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/title"
            android:layout_alignStart="@id/title"
            android:layout_marginTop="16dp"
            android:paddingStart="5dp"
            android:paddingTop="4dp"
            android:paddingEnd="5dp"
            android:textColor="?android:attr/textColorPrimary"
            tools:text="It requires your permission to: \n\n 	• Access your data for all websites. \n\n 	• Acess browser tabs. \n\n 	• Access browser activity during navigation." />

    <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_marginTop="16dp"
            android:layout_toStartOf="@id/allow_button"
            android:text="@string/addon_permissions_dialog_cancel"
            android:textAllCaps="false" />

    <Button
            android:id="@+id/allow_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/permissions"
            android:layout_alignParentEnd="true"
            android:layout_marginStart="8dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="16dp"
            android:layout_marginBottom="16dp"
            android:text="@string/addon_permissions_dialog_add"
            android:textAllCaps="false" />

</RelativeLayout>
 No newline at end of file
+4 −0
Original line number Diff line number Diff line
@@ -22,4 +22,8 @@
    <string name="add_on_details">Details</string>
    <string name="add_on_permissions">Permissions</string>
    <string name="add_on_remove">Remove add-on</string>
    <string name="addon_permissions_dialog_subtitle">It requires your permission to:</string>
    <string name="addon_permissions_dialog_title">Add %1$s?</string>
    <string name="addon_permissions_dialog_add">Add</string>
    <string name="addon_permissions_dialog_cancel">Cancel</string>
</resources>