Unverified Commit 87bd4414 authored by Elise Richards's avatar Elise Richards Committed by GitHub
Browse files

For #14239: Notification for QR scan when permissions have been denied (#14553)

* Show dialog when permissions are denied

* Add qr permissions dialog to search dialog fragment

* Add qr permissions dialog to the pairing screen

* Show dialog after permissions have been denied

* Reset focus after denying permissions

* Show dialog after permissions denied in search frag and par frag

* Use shared preferences to store camera permission state

* Move dialog creation into the search controller and add tests

* Dialog controller implementation and test

* Route to intent with correct activity. Set focus when dismissing dialog

* Get preferences in old search
parent b0729f65
Loading
Loading
Loading
Loading
+55 −0
Original line number Diff line number Diff line
@@ -4,7 +4,13 @@

package org.mozilla.fenix.search

import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.text.SpannableString
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.navigation.NavController
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
@@ -29,6 +35,7 @@ import org.mozilla.fenix.utils.Settings
/**
 * An interface that handles the view manipulation of the Search, triggered by the Interactor
 */
@Suppress("TooManyFunctions")
interface SearchController {
    fun handleUrlCommitted(url: String)
    fun handleEditingCancelled()
@@ -40,6 +47,7 @@ interface SearchController {
    fun handleExistingSessionSelected(session: Session)
    fun handleExistingSessionSelected(tabId: String)
    fun handleSearchShortcutsButtonClicked()
    fun handleCameraPermissionsNeeded()
}

@Suppress("TooManyFunctions", "LongParameterList")
@@ -194,4 +202,51 @@ class DefaultSearchController(
            handleExistingSessionSelected(session)
        }
    }

    /**
     * Creates and shows an [AlertDialog] when camera permissions are needed.
     *
     * In versions above M, [AlertDialog.BUTTON_POSITIVE] takes the user to the app settings. This
     * intent only exists in M and above. Below M, [AlertDialog.BUTTON_POSITIVE] routes to a SUMO
     * help page to find the app settings.
     *
     * [AlertDialog.BUTTON_NEGATIVE] dismisses the dialog.
     */
    override fun handleCameraPermissionsNeeded() {
        val dialog = buildDialog()
        dialog.show()
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    fun buildDialog(): AlertDialog.Builder {
        return AlertDialog.Builder(activity).apply {
            val spannableText = SpannableString(
                activity.resources.getString(R.string.camera_permissions_needed_message)
            )
            setMessage(spannableText)
            setNegativeButton(R.string.camera_permissions_needed_negative_button_text) {
                    dialog: DialogInterface, _ ->
                dialog.cancel()
            }
            setPositiveButton(R.string.camera_permissions_needed_positive_button_text) {
                    dialog: DialogInterface, _ ->
                val intent: Intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                } else {
                    SupportUtils.createCustomTabIntent(
                        activity,
                        SupportUtils.getSumoURLForTopic(
                            activity,
                            SupportUtils.SumoTopic.QR_CAMERA_ACCESS
                        )
                    )
                }
                val uri = Uri.fromParts("package", activity.packageName, null)
                intent.data = uri
                dialog.cancel()
                activity.startActivity(intent)
            }
            create()
        }
    }
}
+33 −6
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.fragment_search.*
import kotlinx.android.synthetic.main.fragment_search.view.*
import kotlinx.android.synthetic.main.search_suggestions_hint.view.*
@@ -50,6 +51,7 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
@@ -219,6 +221,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
                            setNegativeButton(R.string.qr_scanner_dialog_negative) { dialog: DialogInterface, _ ->
                                requireComponents.analytics.metrics.track(Event.QRScannerNavigationDenied)
                                dialog.cancel()
                                resetFocus()
                            }
                            setPositiveButton(R.string.qr_scanner_dialog_positive) { dialog: DialogInterface, _ ->
                                requireComponents.analytics.metrics.track(Event.QRScannerNavigationAllowed)
@@ -229,6 +232,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
                                        from = BrowserDirection.FromSearch
                                    )
                                dialog.dismiss()
                                resetFocus()
                            }
                            create()
                        }.show()
@@ -241,9 +245,20 @@ class SearchFragment : Fragment(), UserInteractionHandler {

        view.search_scan_button.setOnClickListener {
            toolbarView.view.clearFocus()

            val cameraPermissionsDenied = PreferenceManager.getDefaultSharedPreferences(context)
                .getBoolean(
                    getPreferenceKey(R.string.pref_key_camera_permissions),
                    false
                )

            if (cameraPermissionsDenied) {
                searchInteractor.onCameraPermissionsNeeded()
            } else {
                requireComponents.analytics.metrics.track(Event.QRScannerOpened)
                qrFeature.get()?.scan(R.id.container)
            }
        }

        view.search_engines_shortcut_button.setOnClickListener {
            searchInteractor.onSearchShortcutsButtonClicked()
@@ -368,15 +383,19 @@ class SearchFragment : Fragment(), UserInteractionHandler {
    override fun onBackPressed(): Boolean {
        return when {
            qrFeature.onBackPressed() -> {
                toolbarView.view.edit.focus()
                view?.search_scan_button?.isChecked = false
                toolbarView.view.requestFocus()
                resetFocus()
                true
            }
            else -> false
        }
    }

    private fun resetFocus() {
        search_scan_button.isChecked = false
        toolbarView.view.edit.focus()
        toolbarView.view.requestFocus()
    }

    private fun updateSearchWithLabel(searchState: SearchFragmentState) {
        search_engine_shortcut.visibility =
            if (searchState.showSearchShortcuts) View.VISIBLE else View.GONE
@@ -408,8 +427,16 @@ class SearchFragment : Fragment(), UserInteractionHandler {
                context?.let { context: Context ->
                    if (context.isPermissionGranted(Manifest.permission.CAMERA)) {
                        permissionDidUpdate = true
                        PreferenceManager.getDefaultSharedPreferences(context)
                            .edit().putBoolean(
                                getPreferenceKey(R.string.pref_key_camera_permissions), false
                            ).apply()
                    } else {
                        view?.search_scan_button?.isChecked = false
                        PreferenceManager.getDefaultSharedPreferences(context)
                            .edit().putBoolean(
                                getPreferenceKey(R.string.pref_key_camera_permissions), true
                            ).apply()
                        resetFocus()
                    }
                }
            }
+5 −0
Original line number Diff line number Diff line
@@ -13,6 +13,7 @@ import org.mozilla.fenix.search.toolbar.ToolbarInteractor
 * Interactor for the search screen
 * Provides implementations for the AwesomeBarView and ToolbarView
 */
@Suppress("TooManyFunctions")
class SearchInteractor(
    private val searchController: SearchController
) : AwesomeBarInteractor, ToolbarInteractor {
@@ -56,4 +57,8 @@ class SearchInteractor(
    override fun onExistingSessionSelected(tabId: String) {
        searchController.handleExistingSessionSelected(tabId)
    }

    fun onCameraPermissionsNeeded() {
        searchController.handleCameraPermissionsNeeded()
    }
}
+52 −0
Original line number Diff line number Diff line
@@ -4,7 +4,13 @@

package org.mozilla.fenix.searchdialog

import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.text.SpannableString
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.navigation.NavController
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
@@ -186,4 +192,50 @@ class SearchDialogController(
            handleExistingSessionSelected(session)
        }
    }

    /**
     * Creates and shows an [AlertDialog] when camera permissions are needed.
     *
     * In versions above M, [AlertDialog.BUTTON_POSITIVE] takes the user to the app settings. This
     * intent only exists in M and above. Below M, [AlertDialog.BUTTON_POSITIVE] routes to a SUMO
     * help page to find the app settings.
     *
     * [AlertDialog.BUTTON_NEGATIVE] dismisses the dialog.
     */
    override fun handleCameraPermissionsNeeded() {
        val dialog = buildDialog()
        dialog.show()
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    fun buildDialog(): AlertDialog.Builder {
        return AlertDialog.Builder(activity).apply {
            val spannableText = SpannableString(
                activity.resources.getString(R.string.camera_permissions_needed_message)
            )
            setMessage(spannableText)
            setNegativeButton(R.string.camera_permissions_needed_negative_button_text) { _, _ ->
                dismissDialog()
            }
            setPositiveButton(R.string.camera_permissions_needed_positive_button_text) {
                    dialog: DialogInterface, _ ->
                val intent: Intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                } else {
                    SupportUtils.createCustomTabIntent(
                        activity,
                        SupportUtils.getSumoURLForTopic(
                            activity,
                            SupportUtils.SumoTopic.QR_CAMERA_ACCESS
                        )
                    )
                }
                val uri = Uri.fromParts("package", activity.packageName, null)
                intent.data = uri
                dialog.cancel()
                activity.startActivity(intent)
            }
            create()
        }
    }
}
+67 −9
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@

package org.mozilla.fenix.searchdialog

import android.Manifest
import android.app.Activity
import android.app.Dialog
import android.content.Context
@@ -28,11 +29,8 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.fragment_search_dialog.*
import kotlinx.android.synthetic.main.fragment_search_dialog.fill_link_from_clipboard
import kotlinx.android.synthetic.main.fragment_search_dialog.pill_wrapper
import kotlinx.android.synthetic.main.fragment_search_dialog.qr_scan_button
import kotlinx.android.synthetic.main.fragment_search_dialog.toolbar
import kotlinx.android.synthetic.main.fragment_search_dialog.view.*
import kotlinx.android.synthetic.main.search_suggestions_hint.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -44,6 +42,7 @@ import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.content.hasCamera
import mozilla.components.support.ktx.android.content.isPermissionGranted
import mozilla.components.support.ktx.android.content.res.getSpanned
import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.ui.autocomplete.InlineAutocompleteEditText
@@ -55,6 +54,7 @@ import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.search.SearchFragmentAction
@@ -204,9 +204,23 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
            if (!requireContext().hasCamera()) { return@setOnClickListener }

            toolbarView.view.clearFocus()

            val cameraPermissionsDenied =
                PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
                    getPreferenceKey(R.string.pref_key_camera_permissions),
                    false
                )

            if (cameraPermissionsDenied) {
                interactor.onCameraPermissionsNeeded()
                resetFocus()
                view.hideKeyboard()
                toolbarView.view.requestFocus()
            } else {
                requireComponents.analytics.metrics.track(Event.QRScannerOpened)
                qrFeature.get()?.scan(R.id.search_wrapper)
            }
        }

        fill_link_from_clipboard.setOnClickListener {
            (activity as HomeActivity)
@@ -280,6 +294,19 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
        }
    }

    override fun onResume() {
        super.onResume()
        resetFocus()
        toolbarView.view.edit.focus()
    }

    override fun onPause() {
        super.onPause()
        qr_scan_button.isChecked = false
        view?.hideKeyboard()
        toolbarView.view.requestFocus()
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
        if (requestCode == VoiceSearchActivity.SPEECH_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            intent?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.first()?.also {
@@ -293,9 +320,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
    override fun onBackPressed(): Boolean {
        return when {
            qrFeature.onBackPressed() -> {
                toolbarView.view.edit.focus()
                view?.qr_scan_button?.isChecked = false
                toolbarView.view.requestFocus()
                resetFocus()
                true
            }
            else -> {
@@ -350,6 +375,39 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
            })
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        when (requestCode) {
            REQUEST_CODE_CAMERA_PERMISSIONS -> qrFeature.withFeature {
                context?.let { context: Context ->
                    it.onPermissionsResult(permissions, grantResults)
                    if (!context.isPermissionGranted(Manifest.permission.CAMERA)) {
                        PreferenceManager.getDefaultSharedPreferences(context)
                            .edit().putBoolean(
                                getPreferenceKey(R.string.pref_key_camera_permissions), true
                            ).apply()
                        resetFocus()
                    } else {
                        PreferenceManager.getDefaultSharedPreferences(context)
                            .edit().putBoolean(
                                getPreferenceKey(R.string.pref_key_camera_permissions), false
                            ).apply()
                    }
                }
            }
            else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        }
    }

    private fun resetFocus() {
        qr_scan_button.isChecked = false
        toolbarView.view.edit.focus()
        toolbarView.view.requestFocus()
    }

    private fun setupConstraints(view: View) {
        if (view.context.settings().toolbarPosition == ToolbarPosition.BOTTOM) {
            ConstraintSet().apply {
Loading