Closes #1170: Allow user to add a new site exception to site permissions

nightly-build-test
Arturo Mejia 5 years ago committed by Colin Lee
parent 36e9939d9e
commit 22eba72f8f

@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #1238 - Added the ability to edit bookmark folders - #1238 - Added the ability to edit bookmark folders
- #1239 - Added the ability to move bookmark folders - #1239 - Added the ability to move bookmark folders
- #1068 - Adds the ability to quickly copy the URL by long clicking the URLBar - #1068 - Adds the ability to quickly copy the URL by long clicking the URLBar
- #1170: Allow user to add a new site exception to site permissions
### Changed ### Changed
- #1429 - Updated site permissions ui for MVP - #1429 - Updated site permissions ui for MVP
### Removed ### Removed

@ -25,8 +25,10 @@ import kotlinx.android.synthetic.main.component_search.*
import kotlinx.android.synthetic.main.fragment_browser.view.* import kotlinx.android.synthetic.main.fragment_browser.view.*
import kotlinx.android.synthetic.main.fragment_search.* import kotlinx.android.synthetic.main.fragment_search.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
@ -39,12 +41,14 @@ import mozilla.components.feature.session.FullScreenFeature
import mozilla.components.feature.session.SessionFeature import mozilla.components.feature.session.SessionFeature
import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.session.ThumbnailsFeature import mozilla.components.feature.session.ThumbnailsFeature
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.sitepermissions.SitePermissionsFeature import mozilla.components.feature.sitepermissions.SitePermissionsFeature
import mozilla.components.feature.sitepermissions.SitePermissionsRules import mozilla.components.feature.sitepermissions.SitePermissionsRules
import mozilla.components.support.base.feature.BackHandler import mozilla.components.support.base.feature.BackHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.view.enterToImmersiveMode import mozilla.components.support.ktx.android.view.enterToImmersiveMode
import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded
import mozilla.components.support.ktx.kotlin.toUri
import org.mozilla.fenix.BrowsingModeManager import org.mozilla.fenix.BrowsingModeManager
import org.mozilla.fenix.DefaultThemeManager import org.mozilla.fenix.DefaultThemeManager
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
@ -73,9 +77,10 @@ import org.mozilla.fenix.quickactionsheet.QuickActionComponent
import org.mozilla.fenix.settings.quicksettings.QuickSettingsSheetDialogFragment import org.mozilla.fenix.settings.quicksettings.QuickSettingsSheetDialogFragment
import org.mozilla.fenix.utils.ItsNotBrokenSnack import org.mozilla.fenix.utils.ItsNotBrokenSnack
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
import kotlin.coroutines.CoroutineContext
@SuppressWarnings("TooManyFunctions", "LargeClass") @SuppressWarnings("TooManyFunctions", "LargeClass")
class BrowserFragment : Fragment(), BackHandler { class BrowserFragment : Fragment(), BackHandler, CoroutineScope {
private lateinit var toolbarComponent: ToolbarComponent private lateinit var toolbarComponent: ToolbarComponent
private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>() private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>()
@ -88,9 +93,17 @@ class BrowserFragment : Fragment(), BackHandler {
private val fullScreenFeature = ViewBoundFeatureWrapper<FullScreenFeature>() private val fullScreenFeature = ViewBoundFeatureWrapper<FullScreenFeature>()
private val thumbnailsFeature = ViewBoundFeatureWrapper<ThumbnailsFeature>() private val thumbnailsFeature = ViewBoundFeatureWrapper<ThumbnailsFeature>()
private val customTabsIntegration = ViewBoundFeatureWrapper<CustomTabsIntegration>() private val customTabsIntegration = ViewBoundFeatureWrapper<CustomTabsIntegration>()
private lateinit var job: Job
var sessionId: String? = null var sessionId: String? = null
override val coroutineContext: CoroutineContext get() = Dispatchers.IO + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -263,13 +276,7 @@ class BrowserFragment : Fragment(), BackHandler {
) )
} }
toolbarComponent.getView().setOnSiteSecurityClickedListener { toolbarComponent.getView().setOnSiteSecurityClickedListener {
val session = getSessionByIdOrUseSelectedSession() showQuickSettingsDialog()
val quickSettingsSheet = QuickSettingsSheetDialogFragment.newInstance(
url = session.url,
isSecured = session.securityInfo.secure,
isSiteInExceptionList = false
)
quickSettingsSheet.show(requireFragmentManager(), QuickSettingsSheetDialogFragment.FRAGMENT_TAG)
} }
} }
@ -388,6 +395,11 @@ class BrowserFragment : Fragment(), BackHandler {
promptsFeature.withFeature { it.onActivityResult(requestCode, resultCode, data) } promptsFeature.withFeature { it.onActivityResult(requestCode, resultCode, data) }
} }
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
// This method triggers the complexity warning. However it's actually not that hard to understand. // This method triggers the complexity warning. However it's actually not that hard to understand.
@SuppressWarnings("ComplexMethod") @SuppressWarnings("ComplexMethod")
private fun trackToolbarItemInteraction(action: SearchAction.ToolbarMenuItemTapped) { private fun trackToolbarItemInteraction(action: SearchAction.ToolbarMenuItemTapped) {
@ -472,6 +484,26 @@ class BrowserFragment : Fragment(), BackHandler {
} }
} }
private fun showQuickSettingsDialog() {
val session = getSessionByIdOrUseSelectedSession()
val host = requireNotNull(session.url.toUri().host)
launch {
val storage = requireContext().components.storage
val sitePermissions: SitePermissions? = storage.findSitePermissionsBy(host)
launch(Main) {
val quickSettingsSheet = QuickSettingsSheetDialogFragment.newInstance(
url = session.url,
isSecured = session.securityInfo.secure,
sitePermissions = sitePermissions
)
quickSettingsSheet.sitePermissions = sitePermissions
quickSettingsSheet.show(requireFragmentManager(), QuickSettingsSheetDialogFragment.FRAGMENT_TAG)
}
}
}
private fun getSessionByIdOrUseSelectedSession(): Session { private fun getSessionByIdOrUseSelectedSession(): Session {
return if (sessionId != null) { return if (sessionId != null) {
requireNotNull(requireContext().components.core.sessionManager.findSessionById(requireNotNull(sessionId))) requireNotNull(requireContext().components.core.sessionManager.findSessionById(requireNotNull(sessionId)))

@ -17,4 +17,5 @@ class Components(private val context: Context) {
val useCases by lazy { UseCases(context, core.sessionManager, search.searchEngineManager) } val useCases by lazy { UseCases(context, core.sessionManager, search.searchEngineManager) }
val utils by lazy { Utilities(context, core.sessionManager, useCases.sessionUseCases, useCases.searchUseCases) } val utils by lazy { Utilities(context, core.sessionManager, useCases.sessionUseCases, useCases.searchUseCases) }
val analytics by lazy { Analytics(context) } val analytics by lazy { Analytics(context) }
val storage by lazy { Storage(context) }
} }

@ -0,0 +1,44 @@
/* 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.components
import android.content.Context
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.sitepermissions.SitePermissions.Status
import mozilla.components.feature.sitepermissions.SitePermissionsStorage
class Storage(private val context: Context) {
private val permissionsStorage by lazy {
SitePermissionsStorage(context)
}
fun addSitePermissionException(
origin: String,
location: Status,
notification: Status,
microphone: Status,
camera: Status
): SitePermissions {
val sitePermissions = SitePermissions(
origin = origin,
location = location,
camera = camera,
microphone = microphone,
notification = notification,
savedAt = System.currentTimeMillis()
)
permissionsStorage.save(sitePermissions)
return sitePermissions
}
fun findSitePermissionsBy(origin: String): SitePermissions? {
return permissionsStorage.findSitePermissionsBy(origin)
}
fun updateSitePermissions(sitePermissions: SitePermissions) {
permissionsStorage.update(sitePermissions)
}
}

@ -15,7 +15,7 @@ internal fun SitePermissionsRules.Action.toString(context: Context): String {
context.getString(R.string.preference_option_phone_feature_ask_to_allow) context.getString(R.string.preference_option_phone_feature_ask_to_allow)
} }
SitePermissionsRules.Action.BLOCKED -> { SitePermissionsRules.Action.BLOCKED -> {
context.getString(R.string.preference_option_phone_feature_block) context.getString(R.string.preference_option_phone_feature_blocked)
} }
} }
} }
@ -23,13 +23,53 @@ internal fun SitePermissionsRules.Action.toString(context: Context): String {
internal fun SitePermissions.Status.toString(context: Context): String { internal fun SitePermissions.Status.toString(context: Context): String {
return when (this) { return when (this) {
SitePermissions.Status.BLOCKED -> { SitePermissions.Status.BLOCKED -> {
context.getString(R.string.preference_option_phone_feature_block) context.getString(R.string.preference_option_phone_feature_blocked)
} }
SitePermissions.Status.NO_DECISION -> { SitePermissions.Status.NO_DECISION -> {
context.getString(R.string.preference_option_phone_feature_ask_to_allow) context.getString(R.string.preference_option_phone_feature_ask_to_allow)
} }
SitePermissions.Status.ALLOWED -> { SitePermissions.Status.ALLOWED -> {
context.getString(R.string.phone_feature_no_decision) context.getString(R.string.preference_option_phone_feature_allowed)
}
}
}
fun SitePermissionsRules.Action.toStatus(): SitePermissions.Status {
return when (this) {
SitePermissionsRules.Action.BLOCKED -> SitePermissions.Status.BLOCKED
SitePermissionsRules.Action.ASK_TO_ALLOW -> SitePermissions.Status.NO_DECISION
}
}
fun SitePermissions.Status.toggle(): SitePermissions.Status {
return when (this) {
SitePermissions.Status.BLOCKED -> SitePermissions.Status.ALLOWED
SitePermissions.Status.NO_DECISION -> SitePermissions.Status.ALLOWED
SitePermissions.Status.ALLOWED -> SitePermissions.Status.BLOCKED
}
}
fun SitePermissions.toggle(featurePhone: PhoneFeature): SitePermissions {
return when (featurePhone) {
PhoneFeature.CAMERA -> {
copy(
camera = camera.toggle()
)
}
PhoneFeature.LOCATION -> {
copy(
location = location.toggle()
)
}
PhoneFeature.MICROPHONE -> {
copy(
microphone = microphone.toggle()
)
}
PhoneFeature.NOTIFICATION -> {
copy(
notification = notification.toggle()
)
} }
} }
} }

@ -58,6 +58,31 @@ enum class PhoneFeature(val id: Int, val androidPermissionsList: Array<String>)
} }
} }
fun getStatus(sitePermissions: SitePermissions? = null, settings: Settings): SitePermissions.Status {
return when (this) {
CAMERA -> {
sitePermissions?.camera ?: settings
.getSitePermissionsPhoneFeatureCameraAction()
.toStatus()
}
LOCATION -> {
sitePermissions?.location ?: settings
.getSitePermissionsPhoneFeatureLocation()
.toStatus()
}
MICROPHONE -> {
sitePermissions?.microphone ?: settings
.getSitePermissionsPhoneFeatureMicrophoneAction()
.toStatus()
}
NOTIFICATION -> {
sitePermissions?.notification ?: settings
.getSitePermissionsPhoneFeatureNotificationAction()
.toStatus()
}
}
}
companion object { companion object {
fun findFeatureBy(permissions: Array<out String>): PhoneFeature? { fun findFeatureBy(permissions: Array<out String>): PhoneFeature? {
return PhoneFeature.values().find { feature -> return PhoneFeature.values().find { feature ->

@ -4,7 +4,11 @@
package org.mozilla.fenix.settings.quicksettings package org.mozilla.fenix.settings.quicksettings
import android.content.Context
import android.view.ViewGroup import android.view.ViewGroup
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.support.ktx.kotlin.toUri
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.mvi.Action import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change import org.mozilla.fenix.mvi.Change
@ -12,6 +16,9 @@ import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIView import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.mvi.ViewState
import org.mozilla.fenix.settings.PhoneFeature import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.settings.toStatus
import org.mozilla.fenix.settings.toggle
import org.mozilla.fenix.utils.Settings
class QuickSettingsComponent( class QuickSettingsComponent(
private val container: ViewGroup, private val container: ViewGroup,
@ -28,18 +35,23 @@ class QuickSettingsComponent(
mode = QuickSettingsState.Mode.Normal( mode = QuickSettingsState.Mode.Normal(
change.url, change.url,
change.isSecured, change.isSecured,
change.isSiteInExceptionList change.sitePermissions
) )
) )
} }
is QuickSettingsChange.PermissionGranted -> { is QuickSettingsChange.PermissionGranted -> {
state.copy( state.copy(
mode = QuickSettingsState.Mode.ActionLabelUpdated(change.phoneFeature) mode = QuickSettingsState.Mode.ActionLabelUpdated(change.phoneFeature, change.sitePermissions)
) )
} }
QuickSettingsChange.PromptRestarted -> { is QuickSettingsChange.PromptRestarted -> {
state.copy( state.copy(
mode = QuickSettingsState.Mode.CheckPendingFeatureBlockedByAndroid mode = QuickSettingsState.Mode.CheckPendingFeatureBlockedByAndroid(change.sitePermissions)
)
}
is QuickSettingsChange.Stored -> {
state.copy(
mode = QuickSettingsState.Mode.ActionLabelUpdated(change.phoneFeature, change.sitePermissions)
) )
} }
} }
@ -52,28 +64,62 @@ class QuickSettingsComponent(
init { init {
render(reducer) render(reducer)
} }
fun toggleSitePermission(
context: Context,
featurePhone: PhoneFeature,
url: String,
sitePermissions: SitePermissions?
): SitePermissions {
return if (sitePermissions == null) {
val settings = Settings.getInstance(context)
val origin = requireNotNull(url.toUri().host)
var location = settings.getSitePermissionsPhoneFeatureLocation().toStatus()
var camera = settings.getSitePermissionsPhoneFeatureCameraAction().toStatus()
var microphone = settings.getSitePermissionsPhoneFeatureMicrophoneAction().toStatus()
var notification = settings.getSitePermissionsPhoneFeatureNotificationAction().toStatus()
when (featurePhone) {
PhoneFeature.CAMERA -> camera = camera.toggle()
PhoneFeature.LOCATION -> location = location.toggle()
PhoneFeature.MICROPHONE -> microphone = microphone.toggle()
PhoneFeature.NOTIFICATION -> notification = notification.toggle()
}
context.components.storage.addSitePermissionException(origin, location, camera, microphone, notification)
} else {
val updatedSitePermissions = sitePermissions.toggle(featurePhone)
context.components.storage.updateSitePermissions(updatedSitePermissions)
updatedSitePermissions
}
}
} }
data class QuickSettingsState(val mode: Mode) : ViewState { data class QuickSettingsState(val mode: Mode) : ViewState {
sealed class Mode { sealed class Mode {
data class Normal(val url: String, val isSecured: Boolean, val isSiteInExceptionList: Boolean) : Mode() data class Normal(val url: String, val isSecured: Boolean, val sitePermissions: SitePermissions?) : Mode()
data class ActionLabelUpdated(val phoneFeature: PhoneFeature) : Mode() data class ActionLabelUpdated(val phoneFeature: PhoneFeature, val sitePermissions: SitePermissions?) :
object CheckPendingFeatureBlockedByAndroid : Mode() Mode()
data class CheckPendingFeatureBlockedByAndroid(val sitePermissions: SitePermissions?) : Mode()
} }
} }
sealed class QuickSettingsAction : Action { sealed class QuickSettingsAction : Action {
data class SelectBlockedByAndroid(val permissions: Array<String>) : QuickSettingsAction() data class SelectBlockedByAndroid(val permissions: Array<String>) : QuickSettingsAction()
object DismissDialog : QuickSettingsAction() data class TogglePermission(val featurePhone: PhoneFeature) : QuickSettingsAction()
} }
sealed class QuickSettingsChange : Change { sealed class QuickSettingsChange : Change {
data class Change( data class Change(
val url: String, val url: String,
val isSecured: Boolean, val isSecured: Boolean,
val isSiteInExceptionList: Boolean val sitePermissions: SitePermissions?
) : QuickSettingsChange() ) : QuickSettingsChange()
data class PermissionGranted(val phoneFeature: PhoneFeature) : QuickSettingsChange() data class PermissionGranted(val phoneFeature: PhoneFeature, val sitePermissions: SitePermissions?) :
object PromptRestarted : QuickSettingsChange() QuickSettingsChange()
data class PromptRestarted(val sitePermissions: SitePermissions?) : QuickSettingsChange()
data class Stored(val phoneFeature: PhoneFeature, val sitePermissions: SitePermissions?) : QuickSettingsChange()
} }

@ -11,24 +11,44 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import mozilla.components.feature.sitepermissions.SitePermissions
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.mvi.getAutoDisposeObservable
import org.mozilla.fenix.mvi.getManagedEmitter import org.mozilla.fenix.mvi.getManagedEmitter
import org.mozilla.fenix.settings.PhoneFeature import org.mozilla.fenix.settings.PhoneFeature
import kotlin.coroutines.CoroutineContext
private const val KEY_URL = "KEY_URL" private const val KEY_URL = "KEY_URL"
private const val KEY_IS_SECURED = "KEY_IS_SECURED" private const val KEY_IS_SECURED = "KEY_IS_SECURED"
private const val KEY_IS_SITE_IN_EXCEPTION_LIST = "KEY_IS_SITE_IN_EXCEPTION_LIST" private const val KEY_SITE_PERMISSIONS = "KEY_SITE_PERMISSIONS"
private const val REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS = 4 private const val REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS = 4
@SuppressWarnings("TooManyFunctions") @SuppressWarnings("TooManyFunctions")
class QuickSettingsSheetDialogFragment : BottomSheetDialogFragment() { class QuickSettingsSheetDialogFragment : BottomSheetDialogFragment(), CoroutineScope {
private val safeArguments get() = requireNotNull(arguments) private val safeArguments get() = requireNotNull(arguments)
private val url: String by lazy { safeArguments.getString(KEY_URL) } private val url: String by lazy { safeArguments.getString(KEY_URL) }
private val isSecured: Boolean by lazy { safeArguments.getBoolean(KEY_IS_SECURED) } private val isSecured: Boolean by lazy { safeArguments.getBoolean(KEY_IS_SECURED) }
private val isSiteInExceptionList: Boolean by lazy { safeArguments.getBoolean(KEY_IS_SITE_IN_EXCEPTION_LIST) }
private lateinit var quickSettingsComponent: QuickSettingsComponent private lateinit var quickSettingsComponent: QuickSettingsComponent
private lateinit var job: Job
var sitePermissions: SitePermissions?
get() = safeArguments.getParcelable(KEY_SITE_PERMISSIONS)
set(value) {
safeArguments.putParcelable(KEY_SITE_PERMISSIONS, value)
}
override val coroutineContext: CoroutineContext get() = Dispatchers.IO + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_quick_settings_dialog_sheet, container, false) return inflater.inflate(R.layout.fragment_quick_settings_dialog_sheet, container, false)
@ -39,7 +59,7 @@ class QuickSettingsSheetDialogFragment : BottomSheetDialogFragment() {
quickSettingsComponent = QuickSettingsComponent( quickSettingsComponent = QuickSettingsComponent(
rootView as ConstraintLayout, ActionBusFactory.get(this), rootView as ConstraintLayout, ActionBusFactory.get(this),
QuickSettingsState( QuickSettingsState(
QuickSettingsState.Mode.Normal(url, isSecured, isSiteInExceptionList) QuickSettingsState.Mode.Normal(url, isSecured, sitePermissions)
) )
) )
} }
@ -50,7 +70,7 @@ class QuickSettingsSheetDialogFragment : BottomSheetDialogFragment() {
fun newInstance( fun newInstance(
url: String, url: String,
isSecured: Boolean, isSecured: Boolean,
isSiteInExceptionList: Boolean sitePermissions: SitePermissions?
): QuickSettingsSheetDialogFragment { ): QuickSettingsSheetDialogFragment {
val fragment = QuickSettingsSheetDialogFragment() val fragment = QuickSettingsSheetDialogFragment()
@ -59,7 +79,7 @@ class QuickSettingsSheetDialogFragment : BottomSheetDialogFragment() {
with(arguments) { with(arguments) {
putString(KEY_URL, url) putString(KEY_URL, url)
putBoolean(KEY_IS_SECURED, isSecured) putBoolean(KEY_IS_SECURED, isSecured)
putBoolean(KEY_IS_SITE_IN_EXCEPTION_LIST, isSiteInExceptionList) putParcelable(KEY_SITE_PERMISSIONS, sitePermissions)
} }
fragment.arguments = arguments fragment.arguments = arguments
return fragment return fragment
@ -70,10 +90,15 @@ class QuickSettingsSheetDialogFragment : BottomSheetDialogFragment() {
if (arePermissionsGranted(requestCode, grantResults)) { if (arePermissionsGranted(requestCode, grantResults)) {
val feature = requireNotNull(PhoneFeature.findFeatureBy(permissions)) val feature = requireNotNull(PhoneFeature.findFeatureBy(permissions))
getManagedEmitter<QuickSettingsChange>() getManagedEmitter<QuickSettingsChange>()
.onNext(QuickSettingsChange.PermissionGranted(feature)) .onNext(QuickSettingsChange.PermissionGranted(feature, sitePermissions))
} }
} }
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
private fun arePermissionsGranted(requestCode: Int, grantResults: IntArray) = private fun arePermissionsGranted(requestCode: Int, grantResults: IntArray) =
requestCode == REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS && grantResults.all { it == PERMISSION_GRANTED } requestCode == REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS && grantResults.all { it == PERMISSION_GRANTED }
@ -85,13 +110,30 @@ class QuickSettingsSheetDialogFragment : BottomSheetDialogFragment() {
is QuickSettingsAction.SelectBlockedByAndroid -> { is QuickSettingsAction.SelectBlockedByAndroid -> {
requestPermissions(it.permissions, REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS) requestPermissions(it.permissions, REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS)
} }
is QuickSettingsAction.DismissDialog -> dismiss() is QuickSettingsAction.TogglePermission -> {
launch {
sitePermissions = quickSettingsComponent.toggleSitePermission(
context = requireContext(),
featurePhone = it.featurePhone,
url = url,
sitePermissions = sitePermissions
)
launch(Dispatchers.Main) {
getManagedEmitter<QuickSettingsChange>()
.onNext(QuickSettingsChange.Stored(it.featurePhone, sitePermissions))
requireContext().components.useCases.sessionUseCases.reload.invoke()
}
}
}
} }
} }
if (isVisible) { if (isVisible) {
getManagedEmitter<QuickSettingsChange>() getManagedEmitter<QuickSettingsChange>()
.onNext(QuickSettingsChange.PromptRestarted) .onNext(QuickSettingsChange.PromptRestarted(sitePermissions))
} }
} }
} }

@ -4,8 +4,9 @@
package org.mozilla.fenix.settings.quicksettings package org.mozilla.fenix.settings.quicksettings
import android.util.TypedValue
import android.view.View import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
@ -14,9 +15,10 @@ import androidx.core.content.ContextCompat
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Observer import io.reactivex.Observer
import io.reactivex.functions.Consumer import io.reactivex.functions.Consumer
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.sitepermissions.SitePermissions.Status.NO_DECISION
import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes
import mozilla.components.support.ktx.kotlin.toUri import mozilla.components.support.ktx.kotlin.toUri
import org.jetbrains.anko.textColorResource
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.mvi.UIView import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.settings.PhoneFeature import org.mozilla.fenix.settings.PhoneFeature
@ -24,7 +26,6 @@ import org.mozilla.fenix.settings.PhoneFeature.CAMERA
import org.mozilla.fenix.settings.PhoneFeature.LOCATION import org.mozilla.fenix.settings.PhoneFeature.LOCATION
import org.mozilla.fenix.settings.PhoneFeature.MICROPHONE import org.mozilla.fenix.settings.PhoneFeature.MICROPHONE
import org.mozilla.fenix.settings.PhoneFeature.NOTIFICATION import org.mozilla.fenix.settings.PhoneFeature.NOTIFICATION
import org.mozilla.fenix.utils.ItsNotBrokenSnack
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
class QuickSettingsUIView( class QuickSettingsUIView(
@ -38,26 +39,28 @@ class QuickSettingsUIView(
private val securityInfoLabel: TextView private val securityInfoLabel: TextView
private val urlLabel: TextView private val urlLabel: TextView
private val cameraActionLabel: TextView private val cameraActionLabel: TextView
private val cameraLabel: TextView
private val microphoneActionLabel: TextView private val microphoneActionLabel: TextView
private val microphoneLabel: TextView
private val locationActionLabel: TextView private val locationActionLabel: TextView
private val locationLabel: TextView
private val notificationActionLabel: TextView private val notificationActionLabel: TextView
private val notificationLabel: TextView
private val blockedByAndroidPhoneFeatures = mutableListOf<PhoneFeature>() private val blockedByAndroidPhoneFeatures = mutableListOf<PhoneFeature>()
private val context get() = view.context private val context get() = view.context
private val settings: Settings = Settings.getInstance(context) private val settings: Settings = Settings.getInstance(context)
private val toolbarTextColorId by lazy {
val typedValue = TypedValue()
context.theme.resolveAttribute(R.attr.toolbarTextColor, typedValue, true)
typedValue.resourceId
}
init { init {
urlLabel = view.findViewById<AppCompatTextView>(R.id.url) urlLabel = view.findViewById<AppCompatTextView>(R.id.url)
securityInfoLabel = view.findViewById<AppCompatTextView>(R.id.security_info) securityInfoLabel = view.findViewById<AppCompatTextView>(R.id.security_info)
cameraActionLabel = view.findViewById<AppCompatTextView>(R.id.camera_action_label) cameraActionLabel = view.findViewById<AppCompatTextView>(R.id.camera_action_label)
cameraLabel = view.findViewById<AppCompatTextView>(R.id.camera_icon)
microphoneActionLabel = view.findViewById<AppCompatTextView>(R.id.microphone_action_label) microphoneActionLabel = view.findViewById<AppCompatTextView>(R.id.microphone_action_label)
microphoneLabel = view.findViewById<AppCompatTextView>(R.id.microphone_icon)
locationLabel = view.findViewById<AppCompatTextView>(R.id.location_icon)
locationActionLabel = view.findViewById<AppCompatTextView>(R.id.location_action_label) locationActionLabel = view.findViewById<AppCompatTextView>(R.id.location_action_label)
notificationActionLabel = view.findViewById<AppCompatTextView>(R.id.notification_action_label) notificationActionLabel = view.findViewById<AppCompatTextView>(R.id.notification_action_label)
notificationLabel = view.findViewById<AppCompatTextView>(R.id.notification_icon)
} }
override fun updateView() = Consumer<QuickSettingsState> { state -> override fun updateView() = Consumer<QuickSettingsState> { state ->
@ -65,20 +68,20 @@ class QuickSettingsUIView(
is QuickSettingsState.Mode.Normal -> { is QuickSettingsState.Mode.Normal -> {
bindUrl(state.mode.url) bindUrl(state.mode.url)
bindSecurityInfo(state.mode.isSecured) bindSecurityInfo(state.mode.isSecured)
bindPhoneFeatureItem(cameraActionLabel, CAMERA) bindPhoneFeatureItem(cameraActionLabel, CAMERA, state.mode.sitePermissions)
bindPhoneFeatureItem(microphoneActionLabel, MICROPHONE) bindPhoneFeatureItem(microphoneActionLabel, MICROPHONE, state.mode.sitePermissions)
bindPhoneFeatureItem(notificationActionLabel, NOTIFICATION) bindPhoneFeatureItem(notificationActionLabel, NOTIFICATION, state.mode.sitePermissions)
bindPhoneFeatureItem(locationActionLabel, LOCATION) bindPhoneFeatureItem(locationActionLabel, LOCATION, state.mode.sitePermissions)
bindManagePermissionsButton()
} }
is QuickSettingsState.Mode.ActionLabelUpdated -> { is QuickSettingsState.Mode.ActionLabelUpdated -> {
bindPhoneFeatureItem( bindPhoneFeatureItem(
state.mode.phoneFeature.actionLabel, state.mode.phoneFeature.labelAndAction.second,
state.mode.phoneFeature state.mode.phoneFeature,
state.mode.sitePermissions
) )
} }
is QuickSettingsState.Mode.CheckPendingFeatureBlockedByAndroid -> { is QuickSettingsState.Mode.CheckPendingFeatureBlockedByAndroid -> {
checkFeaturesBlockedByAndroid() checkFeaturesBlockedByAndroid(state.mode.sitePermissions)
} }
} }
} }
@ -108,17 +111,41 @@ class QuickSettingsUIView(
securityInfoLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null) securityInfoLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
} }
private fun bindPhoneFeatureItem(actionLabel: TextView, phoneFeature: PhoneFeature) { private fun bindPhoneFeatureItem(
actionLabel: TextView,
phoneFeature: PhoneFeature,
sitePermissions: SitePermissions? = null
) {
if (phoneFeature.shouldBeHidden(sitePermissions)) {
hide(phoneFeature)
return
}
show(phoneFeature)
if (!phoneFeature.isAndroidPermissionGranted(context)) { if (!phoneFeature.isAndroidPermissionGranted(context)) {
handleBlockedByAndroidAction(actionLabel, phoneFeature) handleBlockedByAndroidAction(actionLabel, phoneFeature)
} else { } else {
bindPhoneAction(actionLabel, phoneFeature) bindPhoneAction(actionLabel, phoneFeature, sitePermissions)
} }
} }
private fun show(phoneFeature: PhoneFeature) {
val (label, action) = phoneFeature.labelAndAction
label.visibility = VISIBLE
action.visibility = VISIBLE
}
private fun hide(phoneFeature: PhoneFeature) {
val (label, action) = phoneFeature.labelAndAction
label.visibility = GONE
action.visibility = GONE
}
private fun PhoneFeature.shouldBeHidden(sitePermissions: SitePermissions?): Boolean {
return getStatus(sitePermissions, settings) == NO_DECISION
}
private fun handleBlockedByAndroidAction(actionLabel: TextView, phoneFeature: PhoneFeature) { private fun handleBlockedByAndroidAction(actionLabel: TextView, phoneFeature: PhoneFeature) {
actionLabel.setText(R.string.phone_feature_blocked_by_android) actionLabel.setText(R.string.phone_feature_blocked_by_android)
actionLabel.setTextColor(ContextCompat.getColor(context, R.color.photonBlue50))
actionLabel.tag = phoneFeature actionLabel.tag = phoneFeature
actionLabel.setOnClickListener { actionLabel.setOnClickListener {
val feature = it.tag as PhoneFeature val feature = it.tag as PhoneFeature
@ -131,38 +158,44 @@ class QuickSettingsUIView(
blockedByAndroidPhoneFeatures.add(phoneFeature) blockedByAndroidPhoneFeatures.add(phoneFeature)
} }
private fun bindPhoneAction(actionLabel: TextView, phoneFeature: PhoneFeature) { private fun bindPhoneAction(
actionLabel.text = phoneFeature.getActionLabel(context = context, settings = settings) actionLabel: TextView,
actionLabel.textColorResource = toolbarTextColorId phoneFeature: PhoneFeature,
actionLabel.isEnabled = false sitePermissions: SitePermissions? = null
blockedByAndroidPhoneFeatures.remove(phoneFeature) ) {
} actionLabel.text = phoneFeature.getActionLabel(
context = context,
sitePermissions = sitePermissions,
settings = settings
)
private fun bindManagePermissionsButton() { actionLabel.tag = phoneFeature
val urlLabel = view.findViewById<TextView>(R.id.manage_site_permissions) actionLabel.setOnClickListener {
urlLabel.setOnClickListener { val feature = it.tag as PhoneFeature
actionEmitter.onNext(QuickSettingsAction.DismissDialog) actionEmitter.onNext(
ItsNotBrokenSnack(context).showSnackbar(issueNumber = "1170") QuickSettingsAction.TogglePermission(feature)
)
} }
blockedByAndroidPhoneFeatures.remove(phoneFeature)
} }
private fun checkFeaturesBlockedByAndroid() { private fun checkFeaturesBlockedByAndroid(sitePermissions: SitePermissions?) {
val clonedList = blockedByAndroidPhoneFeatures.toTypedArray() val clonedList = blockedByAndroidPhoneFeatures.toTypedArray()
clonedList.forEach { phoneFeature -> clonedList.forEach { phoneFeature ->
if (phoneFeature.isAndroidPermissionGranted(context)) { if (phoneFeature.isAndroidPermissionGranted(context)) {
val actionLabel = phoneFeature.actionLabel val actionLabel = phoneFeature.labelAndAction.second
bindPhoneAction(actionLabel, phoneFeature) bindPhoneAction(actionLabel, phoneFeature, sitePermissions)
} }
} }
} }
private val PhoneFeature.actionLabel private val PhoneFeature.labelAndAction
get(): TextView { get(): Pair<TextView, TextView> {
return when (this) { return when (this) {
CAMERA -> cameraActionLabel CAMERA -> cameraLabel to cameraActionLabel
LOCATION -> locationActionLabel LOCATION -> locationLabel to locationActionLabel
MICROPHONE -> microphoneActionLabel MICROPHONE -> microphoneLabel to microphoneActionLabel
NOTIFICATION -> notificationActionLabel NOTIFICATION -> notificationLabel to notificationActionLabel
} }
} }
} }

@ -34,7 +34,7 @@
android:id="@+id/block_radio" android:id="@+id/block_radio"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/radio_button_preference_height" android:layout_height="@dimen/radio_button_preference_height"
android:text="@string/preference_option_phone_feature_block" android:text="@string/preference_option_phone_feature_blocked"
android:textAppearance="?android:attr/textAppearanceListItem" android:textAppearance="?android:attr/textAppearanceListItem"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:button="@null" android:button="@null"

@ -121,16 +121,5 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/notification_action_label"/> app:layout_constraintTop_toBottomOf="@id/notification_action_label"/>
<TextView
android:id="@+id/manage_site_permissions"
style="@style/QuickSettingsText"
android:layout_width="wrap_content"
android:layout_height="@dimen/quicksettings_item_height"
android:textColor="@color/photonBlue50"
android:background="?android:attr/selectableItemBackground"
android:text="@string/quick_settings_sheet_manage_site_permissions"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/location_icon"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

@ -36,7 +36,10 @@
<!-- Label that indicates that a permission must be asked always --> <!-- Label that indicates that a permission must be asked always -->
<string name="preference_option_phone_feature_ask_to_allow">Ask to allow</string> <string name="preference_option_phone_feature_ask_to_allow">Ask to allow</string>
<!-- Label that indicates that a permission must be blocked --> <!-- Label that indicates that a permission must be blocked -->
<string name="preference_option_phone_feature_block">Block</string> <string name="preference_option_phone_feature_blocked">Blocked</string>
<!-- Label that indicates that a permission must be allowed -->
<string name="preference_option_phone_feature_allowed">Allowed</string>
<!--Label that indicates a permission is by the Android OS--> <!--Label that indicates a permission is by the Android OS-->
<string name="phone_feature_blocked_by_android">Blocked by Android</string> <string name="phone_feature_blocked_by_android">Blocked by Android</string>
<!--Label that indicates that a user hasn't select a value for a site permission--> <!--Label that indicates that a user hasn't select a value for a site permission-->

@ -252,5 +252,6 @@
<item name="android:paddingEnd">24dp</item> <item name="android:paddingEnd">24dp</item>
<item name="android:gravity">end|center_vertical</item> <item name="android:gravity">end|center_vertical</item>
<item name="android:background">?android:attr/selectableItemBackground</item> <item name="android:background">?android:attr/selectableItemBackground</item>
<item name="android:textColor">@color/photonBlue50</item>
</style> </style>
</resources> </resources>

Loading…
Cancel
Save