For #15079: handle QR permissions when changed in Android settings (#15097)

* Define intent data for activity

* Search dialog shows permissions for allow and deny camera

* Check camera permissions for fxa pairing

* Check camera permissions for old search

* Tests for pairing sync interactor and controller.

* Cleanup

* Use bool pref for setting. Use interfaces and default implementations for the sync interactor and controller.

* Lint
pull/128/head^2
Elise Richards 4 years ago committed by GitHub
parent 8a81c1ee1d
commit 9afe9679d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -26,7 +26,6 @@ import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.fragment_search.* import kotlinx.android.synthetic.main.fragment_search.*
import kotlinx.android.synthetic.main.fragment_search.view.* import kotlinx.android.synthetic.main.fragment_search.view.*
import kotlinx.android.synthetic.main.search_suggestions_hint.view.* import kotlinx.android.synthetic.main.search_suggestions_hint.view.*
@ -51,7 +50,6 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.hideToolbar import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
@ -67,7 +65,6 @@ class SearchFragment : Fragment(), UserInteractionHandler {
private lateinit var toolbarView: ToolbarView private lateinit var toolbarView: ToolbarView
private lateinit var awesomeBarView: AwesomeBarView private lateinit var awesomeBarView: AwesomeBarView
private val qrFeature = ViewBoundFeatureWrapper<QrFeature>() private val qrFeature = ViewBoundFeatureWrapper<QrFeature>()
private var permissionDidUpdate = false
private lateinit var searchStore: SearchFragmentStore private lateinit var searchStore: SearchFragmentStore
private lateinit var searchInteractor: SearchInteractor private lateinit var searchInteractor: SearchInteractor
@ -202,62 +199,26 @@ class SearchFragment : Fragment(), UserInteractionHandler {
search_scan_button.visibility = if (context?.hasCamera() == true) View.VISIBLE else View.GONE search_scan_button.visibility = if (context?.hasCamera() == true) View.VISIBLE else View.GONE
qrFeature.set( qrFeature.set(
QrFeature( createQrFeature(),
requireContext(),
fragmentManager = parentFragmentManager,
onNeedToRequestPermissions = { permissions ->
requestPermissions(permissions, REQUEST_CODE_CAMERA_PERMISSIONS)
},
onScanResult = { result ->
search_scan_button.isChecked = false
activity?.let {
AlertDialog.Builder(it).apply {
val spannable = resources.getSpanned(
R.string.qr_scanner_confirmation_dialog_message,
getString(R.string.app_name) to StyleSpan(BOLD),
result to StyleSpan(ITALIC)
)
setMessage(spannable)
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)
(activity as HomeActivity)
.openToBrowserAndLoad(
searchTermOrURL = result,
newTab = searchStore.state.tabId == null,
from = BrowserDirection.FromSearch
)
dialog.dismiss()
resetFocus()
}
create()
}.show()
requireComponents.analytics.metrics.track(Event.QRScannerPromptDisplayed)
}
}),
owner = this, owner = this,
view = view view = view
) )
view.search_scan_button.setOnClickListener { view.search_scan_button.setOnClickListener {
toolbarView.view.clearFocus() if (requireContext().settings().shouldShowCameraPermissionPrompt) {
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) requireComponents.analytics.metrics.track(Event.QRScannerOpened)
qrFeature.get()?.scan(R.id.container) qrFeature.get()?.scan(R.id.container)
} else {
if (requireContext().isPermissionGranted(Manifest.permission.CAMERA)) {
requireComponents.analytics.metrics.track(Event.QRScannerOpened)
qrFeature.get()?.scan(R.id.container)
} else {
searchInteractor.onCameraPermissionsNeeded()
}
} }
view.hideKeyboard()
search_scan_button.isChecked = false
requireContext().settings().setCameraPermissionNeededState = false
} }
view.search_engines_shortcut_button.setOnClickListener { view.search_engines_shortcut_button.setOnClickListener {
@ -322,6 +283,47 @@ class SearchFragment : Fragment(), UserInteractionHandler {
startPostponedEnterTransition() startPostponedEnterTransition()
} }
private fun createQrFeature(): QrFeature {
return QrFeature(
requireContext(),
fragmentManager = parentFragmentManager,
onNeedToRequestPermissions = { permissions ->
requestPermissions(permissions, REQUEST_CODE_CAMERA_PERMISSIONS)
},
onScanResult = { result ->
search_scan_button.isChecked = false
activity?.let {
AlertDialog.Builder(it).apply {
val spannable = resources.getSpanned(
R.string.qr_scanner_confirmation_dialog_message,
getString(R.string.app_name) to StyleSpan(BOLD),
result to StyleSpan(ITALIC)
)
setMessage(spannable)
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)
(activity as HomeActivity)
.openToBrowserAndLoad(
searchTermOrURL = result,
newTab = searchStore.state.tabId == null,
from = BrowserDirection.FromSearch
)
dialog.dismiss()
resetFocus()
}
create()
}.show()
requireComponents.analytics.metrics.track(Event.QRScannerPromptDisplayed)
}
}
)
}
private fun updateToolbarContentDescription(searchState: SearchFragmentState) { private fun updateToolbarContentDescription(searchState: SearchFragmentState) {
val urlView = toolbarView.view val urlView = toolbarView.view
.findViewById<InlineAutocompleteEditText>(R.id.mozac_browser_toolbar_edit_url_view) .findViewById<InlineAutocompleteEditText>(R.id.mozac_browser_toolbar_edit_url_view)
@ -352,16 +354,11 @@ class SearchFragment : Fragment(), UserInteractionHandler {
searchStore.dispatch(SearchFragmentAction.UpdateShortcutsAvailability(areShortcutsAvailable)) searchStore.dispatch(SearchFragmentAction.UpdateShortcutsAvailability(areShortcutsAvailable))
} }
if (!permissionDidUpdate) {
toolbarView.view.edit.focus()
}
updateClipboardSuggestion( updateClipboardSuggestion(
searchStore.state, searchStore.state,
requireComponents.clipboardHandler.url requireComponents.clipboardHandler.url
) )
permissionDidUpdate = false
hideToolbar() hideToolbar()
} }
@ -423,22 +420,8 @@ class SearchFragment : Fragment(), UserInteractionHandler {
when (requestCode) { when (requestCode) {
REQUEST_CODE_CAMERA_PERMISSIONS -> qrFeature.withFeature { REQUEST_CODE_CAMERA_PERMISSIONS -> qrFeature.withFeature {
it.onPermissionsResult(permissions, grantResults) it.onPermissionsResult(permissions, grantResults)
resetFocus()
context?.let { context: Context -> requireContext().settings().setCameraPermissionNeededState = false
if (context.isPermissionGranted(Manifest.permission.CAMERA)) {
permissionDidUpdate = true
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putBoolean(
getPreferenceKey(R.string.pref_key_camera_permissions), false
).apply()
} else {
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putBoolean(
getPreferenceKey(R.string.pref_key_camera_permissions), true
).apply()
resetFocus()
}
}
} }
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults) else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
} }

@ -29,7 +29,6 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs 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.*
import kotlinx.android.synthetic.main.fragment_search_dialog.view.* import kotlinx.android.synthetic.main.fragment_search_dialog.view.*
import kotlinx.android.synthetic.main.search_suggestions_hint.view.* import kotlinx.android.synthetic.main.search_suggestions_hint.view.*
@ -54,7 +53,6 @@ import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.isKeyboardVisible import org.mozilla.fenix.ext.isKeyboardVisible
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
@ -205,28 +203,34 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
interactor.onSearchShortcutsButtonClicked() interactor.onSearchShortcutsButtonClicked()
} }
qrFeature.set(
createQrFeature(),
owner = this,
view = view
)
qr_scan_button.visibility = if (context?.hasCamera() == true) View.VISIBLE else View.GONE qr_scan_button.visibility = if (context?.hasCamera() == true) View.VISIBLE else View.GONE
qr_scan_button.setOnClickListener { qr_scan_button.setOnClickListener {
if (!requireContext().hasCamera()) { return@setOnClickListener } if (!requireContext().hasCamera()) { return@setOnClickListener }
view.hideKeyboard()
toolbarView.view.clearFocus() toolbarView.view.clearFocus()
val cameraPermissionsDenied = if (requireContext().settings().shouldShowCameraPermissionPrompt) {
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) requireComponents.analytics.metrics.track(Event.QRScannerOpened)
qrFeature.get()?.scan(R.id.search_wrapper) qrFeature.get()?.scan(R.id.search_wrapper)
} else {
if (requireContext().isPermissionGranted(Manifest.permission.CAMERA)) {
requireComponents.analytics.metrics.track(Event.QRScannerOpened)
qrFeature.get()?.scan(R.id.search_wrapper)
} else {
interactor.onCameraPermissionsNeeded()
resetFocus()
view.hideKeyboard()
toolbarView.view.requestFocus()
}
} }
requireContext().settings().setCameraPermissionNeededState = false
} }
fill_link_from_clipboard.setOnClickListener { fill_link_from_clipboard.setOnClickListener {
@ -238,12 +242,6 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
) )
} }
qrFeature.set(
createQrFeature(),
owner = this,
view = view
)
val stubListener = ViewStub.OnInflateListener { _, inflated -> val stubListener = ViewStub.OnInflateListener { _, inflated ->
inflated.learn_more.setOnClickListener { inflated.learn_more.setOnClickListener {
(activity as HomeActivity) (activity as HomeActivity)
@ -379,7 +377,8 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
}.show() }.show()
requireComponents.analytics.metrics.track(Event.QRScannerPromptDisplayed) requireComponents.analytics.metrics.track(Event.QRScannerPromptDisplayed)
} }
}) }
)
} }
override fun onRequestPermissionsResult( override fun onRequestPermissionsResult(
@ -389,21 +388,9 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
) { ) {
when (requestCode) { when (requestCode) {
REQUEST_CODE_CAMERA_PERMISSIONS -> qrFeature.withFeature { REQUEST_CODE_CAMERA_PERMISSIONS -> qrFeature.withFeature {
context?.let { context: Context -> it.onPermissionsResult(permissions, grantResults)
it.onPermissionsResult(permissions, grantResults) resetFocus()
if (!context.isPermissionGranted(Manifest.permission.CAMERA)) { requireContext().settings().setCameraPermissionNeededState = false
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) else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
} }

@ -4,28 +4,21 @@
package org.mozilla.fenix.settings package org.mozilla.fenix.settings
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.VibrationEffect import android.os.VibrationEffect
import android.os.Vibrator import android.os.Vibrator
import android.provider.Settings
import android.text.SpannableString
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.NavHostFragment.findNavController import androidx.navigation.fragment.NavHostFragment.findNavController
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import mozilla.components.feature.qr.QrFeature import mozilla.components.feature.qr.QrFeature
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
@ -65,23 +58,14 @@ class PairFragment : Fragment(R.layout.fragment_pair), UserInteractionHandler {
false false
) )
}, },
scanMessage = R.string.pair_instructions_2), scanMessage = R.string.pair_instructions_2
),
owner = this, owner = this,
view = view view = view
) )
val cameraPermissionsDenied = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(
getPreferenceKey(R.string.pref_key_camera_permissions),
false
)
qrFeature.withFeature { qrFeature.withFeature {
if (cameraPermissionsDenied) { it.scan(R.id.pair_layout)
showPermissionsNeededDialog()
} else {
it.scan(R.id.pair_layout)
}
} }
} }
@ -116,57 +100,10 @@ class PairFragment : Fragment(R.layout.fragment_pair), UserInteractionHandler {
qrFeature.withFeature { qrFeature.withFeature {
it.onPermissionsResult(permissions, grantResults) it.onPermissionsResult(permissions, grantResults)
} }
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putBoolean(
getPreferenceKey(R.string.pref_key_camera_permissions), false
).apply()
} else { } else {
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putBoolean(
getPreferenceKey(R.string.pref_key_camera_permissions), true
).apply()
findNavController().popBackStack(R.id.turnOnSyncFragment, false) findNavController().popBackStack(R.id.turnOnSyncFragment, false)
} }
} }
} }
} }
/**
* 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.
*/
private fun showPermissionsNeededDialog() {
AlertDialog.Builder(requireContext()).apply {
val spannableText = SpannableString(
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(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
} else {
SupportUtils.createCustomTabIntent(
requireContext(),
SupportUtils.getSumoURLForTopic(
requireContext(),
SupportUtils.SumoTopic.QR_CAMERA_ACCESS
)
)
}
dialog.cancel()
startActivity(intent)
}
create()
}.show()
}
} }

@ -0,0 +1,74 @@
/* 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.settings.account
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.text.SpannableString
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.settings.SupportUtils
interface SyncController {
fun handleCameraPermissionsNeeded()
}
/**
* Controller for handling [DefaultSyncInteractor] requests.
*/
class DefaultSyncController(
private val activity: HomeActivity
) : SyncController {
/**
* 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(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()
}
}
}

@ -0,0 +1,20 @@
/* 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.settings.account
interface SyncInteractor {
fun onCameraPermissionsNeeded()
}
/**
* Interactor for [TurnOnSyncFragment].
*
* @param syncController Handles the interactions
*/
class DefaultSyncInteractor(private val syncController: DefaultSyncController) : SyncInteractor {
override fun onCameraPermissionsNeeded() {
syncController.handleCameraPermissionsNeeded()
}
}

@ -4,6 +4,7 @@
package org.mozilla.fenix.settings.account package org.mozilla.fenix.settings.account
import android.Manifest
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -18,15 +19,21 @@ import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.support.ktx.android.content.hasCamera import mozilla.components.support.ktx.android.content.hasCamera
import mozilla.components.support.ktx.android.content.isPermissionGranted
import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
class TurnOnSyncFragment : Fragment(), AccountObserver { class TurnOnSyncFragment : Fragment(), AccountObserver {
private val args by navArgs<TurnOnSyncFragmentArgs>() private val args by navArgs<TurnOnSyncFragmentArgs>()
private lateinit var interactor: DefaultSyncInteractor
private var shouldLoginJustWithEmail = false private var shouldLoginJustWithEmail = false
private var pairWithEmailStarted = false private var pairWithEmailStarted = false
@ -35,6 +42,23 @@ class TurnOnSyncFragment : Fragment(), AccountObserver {
} }
private val paringClickListener = View.OnClickListener { private val paringClickListener = View.OnClickListener {
if (requireContext().settings().shouldShowCameraPermissionPrompt) {
requireComponents.analytics.metrics.track(Event.QRScannerOpened)
navigateToPairFragment()
} else {
if (requireContext().isPermissionGranted(Manifest.permission.CAMERA)) {
requireComponents.analytics.metrics.track(Event.QRScannerOpened)
navigateToPairFragment()
} else {
interactor.onCameraPermissionsNeeded()
view?.hideKeyboard()
}
}
view?.hideKeyboard()
requireContext().settings().setCameraPermissionNeededState = false
}
private fun navigateToPairFragment() {
val directions = TurnOnSyncFragmentDirections.actionTurnOnSyncFragmentToPairFragment() val directions = TurnOnSyncFragmentDirections.actionTurnOnSyncFragmentToPairFragment()
requireView().findNavController().navigate(directions) requireView().findNavController().navigate(directions)
requireComponents.analytics.metrics.track(Event.SyncAuthScanPairing) requireComponents.analytics.metrics.track(Event.SyncAuthScanPairing)
@ -89,6 +113,11 @@ class TurnOnSyncFragment : Fragment(), AccountObserver {
getString(R.string.sign_in_instructions), getString(R.string.sign_in_instructions),
HtmlCompat.FROM_HTML_MODE_LEGACY HtmlCompat.FROM_HTML_MODE_LEGACY
) )
interactor = DefaultSyncInteractor(
DefaultSyncController(activity = activity as HomeActivity)
)
return view return view
} }

@ -60,7 +60,6 @@ class Settings(private val appContext: Context) : PreferencesHolder {
private const val ALLOWED_INT = 2 private const val ALLOWED_INT = 2
private const val CFR_COUNT_CONDITION_FOCUS_INSTALLED = 1 private const val CFR_COUNT_CONDITION_FOCUS_INSTALLED = 1
private const val CFR_COUNT_CONDITION_FOCUS_NOT_INSTALLED = 3 private const val CFR_COUNT_CONDITION_FOCUS_NOT_INSTALLED = 3
private const val MIN_DAYS_SINCE_FEEDBACK_PROMPT = 120
const val ONE_DAY_MS = 60 * 60 * 24 * 1000L const val ONE_DAY_MS = 60 * 60 * 24 * 1000L
const val ONE_WEEK_MS = 60 * 60 * 24 * 7 * 1000L const val ONE_WEEK_MS = 60 * 60 * 24 * 7 * 1000L
@ -766,6 +765,24 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = true default = true
) )
/**
* Used in [SearchDialogFragment.kt], [SearchFragment.kt] (deprecated), and [PairFragment.kt]
* to see if we need to check for camera permissions before using the QR code scanner.
*/
var shouldShowCameraPermissionPrompt by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_camera_permissions_needed),
default = true
)
/**
* Sets the state of permissions that have been checked, where [false] denotes already checked
* and [true] denotes needing to check. See [shouldShowCameraPermissionPrompt].
*/
var setCameraPermissionNeededState by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_camera_permissions_needed),
default = true
)
var shouldPromptToSaveLogins by booleanPreference( var shouldPromptToSaveLogins by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_save_logins), appContext.getPreferenceKey(R.string.pref_key_save_logins),
default = true default = true

@ -225,5 +225,5 @@
<string name="pref_key_close_tabs_after_one_week" translatable="false">pref_key_close_tabs_after_one_week</string> <string name="pref_key_close_tabs_after_one_week" translatable="false">pref_key_close_tabs_after_one_week</string>
<string name="pref_key_close_tabs_after_one_month" translatable="false">pref_key_close_tabs_after_one_month</string> <string name="pref_key_close_tabs_after_one_month" translatable="false">pref_key_close_tabs_after_one_month</string>
<string name="pref_key_camera_permissions" translatable="false">pref_key_camera_permissions</string> <string name="pref_key_camera_permissions_needed" translatable="false">pref_key_camera_permissions_needed</string>
</resources> </resources>

@ -106,4 +106,13 @@ class SearchInteractorTest {
searchController.handleExistingSessionSelected(session) searchController.handleExistingSessionSelected(session)
} }
} }
@Test
fun onCameraPermissionsNeeded() {
interactor.onCameraPermissionsNeeded()
verify {
searchController.handleCameraPermissionsNeeded()
}
}
} }

@ -0,0 +1,40 @@
/* 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.settings.account
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.search.AlertDialogBuilder
class DefaultSyncControllerTest {
private lateinit var syncController: DefaultSyncController
@MockK(relaxed = true) private lateinit var activity: HomeActivity
@Before
fun setUp() {
MockKAnnotations.init(this)
syncController = DefaultSyncController(activity)
}
@Test
fun `show camera permissions needed dialog`() {
val dialogBuilder: AlertDialogBuilder = mockk(relaxed = true)
val spyController = spyk(syncController)
every { spyController.buildDialog() } returns dialogBuilder
spyController.handleCameraPermissionsNeeded()
verify { dialogBuilder.show() }
}
}

@ -0,0 +1,31 @@
/* 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.settings.account
import io.mockk.mockk
import io.mockk.verify
import org.junit.Before
import org.junit.Test
class DefaultSyncInteractorTest {
private lateinit var syncInteractor: DefaultSyncInteractor
private lateinit var syncController: DefaultSyncController
@Before
fun setUp() {
syncController = mockk(relaxed = true)
syncInteractor = DefaultSyncInteractor(syncController)
}
@Test
fun onCameraPermissionsNeeded() {
syncInteractor.onCameraPermissionsNeeded()
verify {
syncController.handleCameraPermissionsNeeded()
}
}
}
Loading…
Cancel
Save