1
0
Fork 0

For #4126 - Handle all business logic with Interactors and a Controller

Combined all Stores into one for all Views shown in on Fragment.
Used a static `createStore()` which will build the initial state residing
inside the Store and not in the Fragment as to decouple the Fragment from the
business logic needed to build all the needed initial States.
Added Interactors that handle a MVI View's business logic for
TrackingProtectionView and WebsitePermissionsView.
WebsiteInfoView doesn't register any user input events and does not have any
reason to change while it is displayed so it does not have an Interactor.
The two Interactors will delegate Fragment's QuickSettingsController for
complex Android interactions, communication with other app features or for
Store updates.
Also refactored the stubs from the previous commit so that with this commit the
the quicksettings feature should all be working now based on lib-state.
master
Mugurell 2019-10-16 20:15:58 +03:00 committed by Emily Kager
parent f1f74bc3d6
commit d70afcaa90
13 changed files with 790 additions and 276 deletions

View File

@ -0,0 +1,167 @@
/* 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.quicksettings
import android.content.Context
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.launch
import mozilla.components.browser.session.Session
import mozilla.components.feature.session.SessionUseCases.ReloadUrlUseCase
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.tabs.TabsUseCases.AddNewTabUseCase
import mozilla.components.support.base.feature.OnNeedToRequestPermissions
import org.mozilla.fenix.browser.BrowserFragment
import org.mozilla.fenix.components.PermissionStorage
import org.mozilla.fenix.exceptions.ExceptionDomains
import org.mozilla.fenix.ext.tryGetHostFromUrl
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.settings.quicksettings.ext.shouldBeEnabled
import org.mozilla.fenix.settings.toggle
import org.mozilla.fenix.utils.Settings
interface QuickSettingsController {
fun handleTrackingProtectionToggled(websiteUrl: String, trackingEnabled: Boolean)
fun handleTrackingProtectionSettingsSelected()
fun handleReportTrackingProblem(websiteUrl: String)
fun handleTrackingProtectionShown()
fun handlePermissionsShown()
fun handlePermissionToggled(permission: WebsitePermission)
fun handleAndroidPermissionGranted(feature: PhoneFeature)
}
@Suppress("TooManyFunctions")
class DefaultQuickSettingsController(
private val context: Context,
private val quickSettingsStore: QuickSettingsFragmentStore,
private val coroutineScope: CoroutineScope,
private val navController: NavController,
private val session: Session?,
private var sitePermissions: SitePermissions?,
private val settings: Settings,
private val permissionStorage: PermissionStorage,
private val trackingExceptions: ExceptionDomains,
private val reload: ReloadUrlUseCase,
private val addNewTab: AddNewTabUseCase,
private val requestRuntimePermissions: OnNeedToRequestPermissions = { },
private val reportSiteIssue: () -> Unit,
private val displayTrackingProtection: () -> Unit,
private val displayPermissions: () -> Unit,
private val dismiss: () -> Unit
) : QuickSettingsController {
override fun handleTrackingProtectionToggled(
websiteUrl: String,
trackingEnabled: Boolean
) {
val host = websiteUrl.tryGetHostFromUrl()
trackingExceptions.toggle(host)
reload(session)
quickSettingsStore.dispatch(
TrackingProtectionAction.TrackingProtectionToggled(trackingEnabled)
)
}
override fun handleTrackingProtectionSettingsSelected() {
val directions =
QuickSettingsSheetDialogFragmentDirections
.actionQuickSettingsSheetDialogFragmentToTrackingProtectionFragment()
navController.navigate(directions)
}
@ExperimentalCoroutinesApi
@UseExperimental(ObsoleteCoroutinesApi::class)
override fun handleReportTrackingProblem(websiteUrl: String) {
val reportUrl = String.format(BrowserFragment.REPORT_SITE_ISSUE_URL, websiteUrl)
addNewTab(reportUrl)
if (session?.isCustomTabSession() == true) {
reportSiteIssue()
}
dismiss()
}
override fun handleTrackingProtectionShown() {
displayTrackingProtection()
}
override fun handlePermissionsShown() {
displayPermissions()
}
override fun handlePermissionToggled(permission: WebsitePermission) {
val featureToggled = permission.getBackingFeature()
when (permission.isBlockedByAndroid) {
true -> handleAndroidPermissionRequest(featureToggled.androidPermissionsList)
false -> {
sitePermissions = sitePermissions!!.toggle(featureToggled).also {
handlePermissionsChange(it)
}
quickSettingsStore.dispatch(
WebsitePermissionAction.TogglePermission(
permission,
featureToggled.getActionLabel(context, sitePermissions, settings),
featureToggled.shouldBeEnabled(context, sitePermissions, settings)
)
)
}
}
}
override fun handleAndroidPermissionGranted(feature: PhoneFeature) {
quickSettingsStore.dispatch(
WebsitePermissionAction.TogglePermission(
feature.getCorrespondingPermission(),
feature.getActionLabel(context, sitePermissions, settings),
feature.shouldBeEnabled(context, sitePermissions, settings)
)
)
}
private fun handleAndroidPermissionRequest(requestedPermissions: Array<String>) {
requestRuntimePermissions(requestedPermissions)
}
private fun handlePermissionsChange(updatedPermissions: SitePermissions) {
coroutineScope.launch(Dispatchers.IO) {
permissionStorage.updateSitePermissions(updatedPermissions)
reload(session)
}
}
private fun WebsitePermission.getBackingFeature(): PhoneFeature = when (this) {
is WebsitePermission.Camera -> PhoneFeature.CAMERA
is WebsitePermission.Microphone -> PhoneFeature.MICROPHONE
is WebsitePermission.Notification -> PhoneFeature.NOTIFICATION
is WebsitePermission.Location -> PhoneFeature.LOCATION
}
private fun PhoneFeature.getCorrespondingPermission(): WebsitePermission {
val defaultStatus = ""
val defaultEnabled = false
val defaultVisible = false
val defaultBlockedByAndroid = false
val defaultWebsitePermission: WebsitePermission? = null
return when (this) {
PhoneFeature.CAMERA -> WebsitePermission.Camera(
defaultStatus, defaultVisible, defaultEnabled, defaultBlockedByAndroid)
PhoneFeature.LOCATION -> WebsitePermission.Location(
defaultStatus, defaultVisible, defaultEnabled, defaultBlockedByAndroid)
PhoneFeature.MICROPHONE -> WebsitePermission.Microphone(
defaultStatus, defaultVisible, defaultEnabled, defaultBlockedByAndroid)
PhoneFeature.NOTIFICATION -> WebsitePermission.Notification(
defaultStatus, defaultVisible, defaultEnabled, defaultBlockedByAndroid)
PhoneFeature.AUTOPLAY -> defaultWebsitePermission!! // fail-fast
}
}
}

View File

@ -0,0 +1,382 @@
/* 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.quicksettings
import android.content.Context
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.settings.quicksettings.ext.shouldBeEnabled
import org.mozilla.fenix.settings.quicksettings.ext.shouldBeVisible
import org.mozilla.fenix.utils.Settings
class QuickSettingsFragmentStore(
initialState: QuickSettingsFragmentState
) : Store<QuickSettingsFragmentState, QuickSettingsFragmentAction>(
initialState,
::quickSettingsFragmentReducer
) {
companion object {
private val getSecuredWebsiteUiValues = Triple(
R.string.quick_settings_sheet_secure_connection,
R.drawable.mozac_ic_lock,
R.color.photonGreen50
)
private val getInsecureWebsiteUiValues = Triple(
R.string.quick_settings_sheet_insecure_connection,
R.drawable.mozac_ic_globe,
R.color.photonRed50
)
@Suppress("LongParameterList")
fun createStore(
context: Context,
websiteUrl: String,
isSecured: Boolean,
isTrackingProtectionOn: Boolean,
permissions: SitePermissions?,
settings: Settings
) = QuickSettingsFragmentStore(
QuickSettingsFragmentState(
trackingProtectionState = createTrackingProtectionState(websiteUrl, isTrackingProtectionOn, settings),
webInfoState = createWebsiteInfoState(websiteUrl, isSecured),
websitePermissionsState = createWebsitePermissionState(context, permissions, settings)
)
)
private fun createTrackingProtectionState(
websiteUrl: String,
isTrackingProtectionOn: Boolean,
settings: Settings
) = TrackingProtectionState(
isVisible = FeatureFlags.etpCategories.not(),
isTrackingProtectionEnabledPerApp = settings.shouldUseTrackingProtection,
websiteUrl = websiteUrl,
isTrackingProtectionEnabledPerWebsite = isTrackingProtectionOn
)
private fun createWebsiteInfoState(
websiteUrl: String,
isSecured: Boolean
): WebsiteInfoState {
val (stringRes, iconRes, colorRes) = when (isSecured) {
true -> getSecuredWebsiteUiValues
false -> getInsecureWebsiteUiValues
}
return WebsiteInfoState(websiteUrl, stringRes, iconRes, colorRes)
}
private fun createWebsitePermissionState(
context: Context,
permissions: SitePermissions?,
settings: Settings
): WebsitePermissionsState {
val cameraPermission = PhoneFeature.CAMERA.toWebsitePermission(context, permissions, settings)
val microphonePermission = PhoneFeature.MICROPHONE.toWebsitePermission(context, permissions, settings)
val notificationPermission = PhoneFeature.NOTIFICATION.toWebsitePermission(context, permissions, settings)
val locationPermission = PhoneFeature.LOCATION.toWebsitePermission(context, permissions, settings)
val shouldBeVisible = cameraPermission.isVisible || microphonePermission.isVisible ||
notificationPermission.isVisible || locationPermission.isVisible
return WebsitePermissionsState(shouldBeVisible, cameraPermission, microphonePermission,
notificationPermission, locationPermission
)
}
private fun PhoneFeature.toWebsitePermission(
context: Context,
permissions: SitePermissions?,
settings: Settings
): WebsitePermission {
val status = getPermissionStatus(context, permissions, settings)
val nonexistentPermission: WebsitePermission? = null
return when (this) {
PhoneFeature.CAMERA -> WebsitePermission.Camera(
status.status, status.isVisible, status.isEnabled, status.isBlockedByAndroid
)
PhoneFeature.LOCATION -> WebsitePermission.Location(
status.status, status.isVisible, status.isEnabled, status.isBlockedByAndroid
)
PhoneFeature.MICROPHONE -> WebsitePermission.Microphone(
status.status, status.isVisible, status.isEnabled, status.isBlockedByAndroid
)
PhoneFeature.NOTIFICATION -> WebsitePermission.Notification(
status.status, status.isVisible, status.isEnabled, status.isBlockedByAndroid
)
PhoneFeature.AUTOPLAY -> nonexistentPermission!! // fail-fast
}
}
private fun PhoneFeature.getPermissionStatus(
context: Context,
permissions: SitePermissions?,
settings: Settings
) = PermissionStatus(
status = getActionLabel(context, permissions, settings),
isVisible = shouldBeVisible(permissions, settings),
isEnabled = shouldBeEnabled(context, permissions, settings),
isBlockedByAndroid = !isAndroidPermissionGranted(context)
)
private data class PermissionStatus(
val status: String,
val isVisible: Boolean,
val isEnabled: Boolean,
val isBlockedByAndroid: Boolean
)
}
}
// -------------------------------------------------------------------------------------------------
// States
// -------------------------------------------------------------------------------------------------
data class QuickSettingsFragmentState(
val trackingProtectionState: TrackingProtectionState,
val webInfoState: WebsiteInfoState,
val websitePermissionsState: WebsitePermissionsState
) : State
data class TrackingProtectionState(
val isVisible: Boolean,
val websiteUrl: String,
val isTrackingProtectionEnabledPerApp: Boolean,
val isTrackingProtectionEnabledPerWebsite: Boolean
) : State
data class WebsiteInfoState(
val websiteUrl: String,
@StringRes val securityInfoRes: Int,
@DrawableRes val iconRes: Int,
@ColorRes val iconTintRes: Int
) : State
data class WebsitePermissionsState(
val isVisible: Boolean,
val camera: WebsitePermission,
val microphone: WebsitePermission,
val notification: WebsitePermission,
val location: WebsitePermission
) : State
sealed class WebsitePermission {
abstract val status: String
abstract val isVisible: Boolean
abstract val isEnabled: Boolean
abstract val isBlockedByAndroid: Boolean
abstract fun copy(
status: String = this.status,
isVisible: Boolean = this.isVisible,
isEnabled: Boolean = this.isEnabled,
isBlockedByAndroid: Boolean = this.isBlockedByAndroid
): WebsitePermission
data class Camera(
override val status: String,
override val isVisible: Boolean,
override val isEnabled: Boolean,
override val isBlockedByAndroid: Boolean,
val name: String = "Camera" // helps to resolve the overload resolution ambiguity for the copy() method
) : WebsitePermission() {
override fun copy(
status: String,
isVisible: Boolean,
isEnabled: Boolean,
isBlockedByAndroid: Boolean
) = copy(
status = status,
isVisible = isVisible,
isEnabled = isEnabled,
isBlockedByAndroid = isBlockedByAndroid,
name = name
)
}
data class Microphone(
override val status: String,
override val isVisible: Boolean,
override val isEnabled: Boolean,
override val isBlockedByAndroid: Boolean,
val name: String = "Microphone" // helps to resolve the overload resolution ambiguity for the copy() method
) : WebsitePermission() {
override fun copy(
status: String,
isVisible: Boolean,
isEnabled: Boolean,
isBlockedByAndroid: Boolean
) = copy(
status = status,
isVisible = isVisible,
isEnabled = isEnabled,
isBlockedByAndroid = isBlockedByAndroid,
name = name
)
}
data class Notification(
override val status: String,
override val isVisible: Boolean,
override val isEnabled: Boolean,
override val isBlockedByAndroid: Boolean,
val name: String = "Notification" // helps to resolve the overload resolution ambiguity for the copy() method
) : WebsitePermission() {
override fun copy(
status: String,
isVisible: Boolean,
isEnabled: Boolean,
isBlockedByAndroid: Boolean
) = copy(
status = status,
isVisible = isVisible,
isEnabled = isEnabled,
isBlockedByAndroid = isBlockedByAndroid,
name = name
)
}
data class Location(
override val status: String,
override val isVisible: Boolean,
override val isEnabled: Boolean,
override val isBlockedByAndroid: Boolean,
val name: String = "Location" // helps to resolve the overload resolution ambiguity for the copy() method
) : WebsitePermission() {
override fun copy(
status: String,
isVisible: Boolean,
isEnabled: Boolean,
isBlockedByAndroid: Boolean
) = copy(
status = status,
isVisible = isVisible,
isEnabled = isEnabled,
isBlockedByAndroid = isBlockedByAndroid,
name = name
)
}
}
// -------------------------------------------------------------------------------------------------
// Actions
// -------------------------------------------------------------------------------------------------
sealed class QuickSettingsFragmentAction : Action
sealed class TrackingProtectionAction : QuickSettingsFragmentAction() {
class TrackingProtectionToggled(val trackingEnabled: Boolean) : TrackingProtectionAction()
}
sealed class WebsiteInfoAction : QuickSettingsFragmentAction()
sealed class WebsitePermissionAction : QuickSettingsFragmentAction() {
class TogglePermission(
val websitePermission: WebsitePermission,
val updatedStatus: String,
val updatedEnabledStatus: Boolean
) : WebsitePermissionAction()
}
// -------------------------------------------------------------------------------------------------
// Reducers
// -------------------------------------------------------------------------------------------------
fun quickSettingsFragmentReducer(
state: QuickSettingsFragmentState,
action: QuickSettingsFragmentAction
): QuickSettingsFragmentState {
return when (action) {
is TrackingProtectionAction -> state.copy(
trackingProtectionState = TrackingProtectionStateReducer.reduce(
state.trackingProtectionState,
action
)
)
is WebsiteInfoAction -> state.copy(
webInfoState = WebsiteInfoStateReducer.reduce(
state.webInfoState,
action
)
)
is WebsitePermissionAction -> state.copy(
websitePermissionsState = WebsitePermissionsStateReducer.reduce(
state.websitePermissionsState,
action
)
)
}
}
object TrackingProtectionStateReducer {
fun reduce(
state: TrackingProtectionState,
action: TrackingProtectionAction
): TrackingProtectionState {
return when (action) {
is TrackingProtectionAction.TrackingProtectionToggled -> state.copy(
isTrackingProtectionEnabledPerWebsite = action.trackingEnabled
)
}
}
}
@Suppress("UNUSED_PARAMETER")
object WebsiteInfoStateReducer {
fun reduce(
state: WebsiteInfoState,
action: WebsiteInfoAction
): WebsiteInfoState {
// There is no possible action that can change this View's state while it is displayed to the user.
// Everytime the View is recreated it starts with a fresh state. This is the only way to display
// something different.
return state
}
}
object WebsitePermissionsStateReducer {
fun reduce(
state: WebsitePermissionsState,
action: WebsitePermissionAction
): WebsitePermissionsState {
return when (action) {
is WebsitePermissionAction.TogglePermission -> {
when (action.websitePermission) {
is WebsitePermission.Camera -> state.copy(
camera = state.camera.copy(
status = action.updatedStatus,
isEnabled = action.updatedEnabledStatus
)
)
is WebsitePermission.Microphone -> state.copy(
microphone = state.microphone.copy(
status = action.updatedStatus,
isEnabled = action.updatedEnabledStatus
)
)
is WebsitePermission.Notification -> state.copy(
notification = state.notification.copy(
status = action.updatedStatus,
isEnabled = action.updatedEnabledStatus
)
)
is WebsitePermission.Location -> state.copy(
location = state.location.copy(
status = action.updatedStatus,
isEnabled = action.updatedEnabledStatus
)
)
}
}
}
}
}

View File

@ -0,0 +1,34 @@
/* 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.quicksettings
class QuickSettingsInteractor(
private val controller: QuickSettingsController
) : WebsitePermissionInteractor, TrackingProtectionInteractor {
override fun onReportProblemSelected(websiteUrl: String) {
controller.handleReportTrackingProblem(websiteUrl)
}
override fun onProtectionToggled(websiteUrl: String, trackingEnabled: Boolean) {
controller.handleTrackingProtectionToggled(websiteUrl, trackingEnabled)
}
override fun onProtectionSettingsSelected() {
controller.handleTrackingProtectionSettingsSelected()
}
override fun onTrackingProtectionShown() {
controller.handleTrackingProtectionShown()
}
override fun onPermissionsShown() {
controller.handlePermissionsShown()
}
override fun onPermissionToggled(permissionState: WebsitePermission) {
controller.handlePermissionToggled(permissionState)
}
}

View File

@ -5,6 +5,8 @@
package org.mozilla.fenix.settings.quicksettings
import android.app.Dialog
import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
@ -17,28 +19,30 @@ import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import kotlinx.android.synthetic.main.fragment_quick_settings_dialog_sheet.*
import kotlinx.android.synthetic.main.fragment_quick_settings_dialog_sheet.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.exceptions.ExceptionDomains
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.utils.Settings
import com.google.android.material.R as MaterialR
@ObsoleteCoroutinesApi
@SuppressWarnings("TooManyFunctions")
class QuickSettingsSheetDialogFragment : AppCompatDialogFragment() {
private lateinit var websiteInfoStore: WebsiteInfoStore
private lateinit var websitePermissionsStore: WebsitePermissionsStore
private lateinit var websiteTrackingProtectionStore: TrackingProtectionStore
private lateinit var quickSettingsStore: QuickSettingsFragmentStore
private lateinit var quickSettingsController: QuickSettingsController
private lateinit var websiteInfoView: WebsiteInfoView
private lateinit var websitePermissionsView: WebsitePermissionsView
private lateinit var websiteTrackingProtectionView: TrackingProtectionView
private lateinit var interactor: QuickSettingsInteractor
private val safeArguments get() = requireNotNull(arguments)
private val promptGravity: Int by lazy { QuickSettingsSheetDialogFragmentArgs.fromBundle(safeArguments).gravity }
@ -48,29 +52,44 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment() {
savedInstanceState: Bundle?
): View {
val context = context!!
val args = QuickSettingsSheetDialogFragmentArgs.fromBundle(safeArguments)
val rootView = inflateRootView(container)
websitePermissionsStore = WebsitePermissionsStore.createStore(
context!!, args.sitePermissions, Settings.getInstance(context!!)
quickSettingsStore = QuickSettingsFragmentStore.createStore(
context = context,
websiteUrl = args.url,
isSecured = args.isSecured,
isTrackingProtectionOn = args.isTrackingProtectionOn,
permissions = args.sitePermissions,
settings = Settings.getInstance(context)
)
websiteInfoStore = WebsiteInfoStore.createStore(args.url, args.isSecured)
if (!FeatureFlags.etpCategories) {
websiteTrackingProtectionStore =
TrackingProtectionStore.createStore(
args.url,
args.isTrackingProtectionOn,
context!!.settings()
)
websiteTrackingProtectionView =
TrackingProtectionView(rootView.trackingProtectionLayout)
} else {
rootView.trackingProtectionGroup.isVisible = false
}
quickSettingsController = DefaultQuickSettingsController(
context = context,
quickSettingsStore = quickSettingsStore,
coroutineScope = lifecycleScope,
navController = findNavController(),
session = context.components.core.sessionManager.findSessionById(args.sessionId),
sitePermissions = args.sitePermissions,
settings = Settings.getInstance(context),
permissionStorage = context.components.core.permissionStorage,
trackingExceptions = ExceptionDomains(context),
reload = context.components.useCases.sessionUseCases.reload,
addNewTab = context.components.useCases.tabsUseCases.addTab,
requestRuntimePermissions = { permissions ->
requestPermissions(permissions, REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS)
},
reportSiteIssue = ::launchIntentReceiver,
displayTrackingProtection = ::showTrackingProtectionView,
displayPermissions = ::showPermissionsView,
dismiss = ::dismiss
)
interactor = QuickSettingsInteractor(quickSettingsController)
websiteTrackingProtectionView = TrackingProtectionView(rootView.trackingProtectionLayout, interactor)
websiteInfoView = WebsiteInfoView(rootView.websiteInfoLayout)
websitePermissionsView = WebsitePermissionsView(rootView.websitePermissionsLayout)
websitePermissionsView = WebsitePermissionsView(rootView.websitePermissionsLayout, interactor)
return rootView
}
@ -106,10 +125,22 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
consumeFrom(websiteInfoStore) { websiteInfoView.update(it) }
consumeFrom(websitePermissionsStore) { websitePermissionsView.update(it) }
if (::websiteTrackingProtectionStore.isInitialized) {
consumeFrom(websiteTrackingProtectionStore) { websiteTrackingProtectionView.update(it) }
consumeFrom(quickSettingsStore) {
websiteInfoView.update(it.webInfoState)
websiteTrackingProtectionView.update(it.trackingProtectionState)
websitePermissionsView.update(it.websitePermissionsState)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
if (arePermissionsGranted(requestCode, grantResults)) {
PhoneFeature.findFeatureBy(permissions)?.let {
quickSettingsController.handleAndroidPermissionGranted(it)
}
}
}
@ -130,4 +161,27 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment() {
}
return this
}
private fun arePermissionsGranted(requestCode: Int, grantResults: IntArray) =
requestCode == REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS && grantResults.all { it == PERMISSION_GRANTED }
private fun showTrackingProtectionView() {
trackingProtectionGroup.isVisible = true
}
private fun showPermissionsView() {
websitePermissionsGroup.isVisible = true
}
private fun launchIntentReceiver() {
context?.let { context ->
val intent = Intent(context, IntentReceiverActivity::class.java)
intent.action = Intent.ACTION_VIEW
context.startActivity(intent)
}
}
private companion object {
const val REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS = 4
}
}

View File

@ -1,51 +0,0 @@
/* 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.quicksettings
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import org.mozilla.fenix.utils.Settings
class TrackingProtectionStore(
val initialState: TrackingProtectionState
) : Store<TrackingProtectionState, TrackingProtectionAction>(
initialState, ::trackingProtectionReducer
) {
companion object {
fun createStore(
url: String,
isTrackingProtectionOn: Boolean,
settings: Settings
) = TrackingProtectionStore(
TrackingProtectionState(
websiteUrl = url,
isTrackingProtectionEnabledPerApp = settings.shouldUseTrackingProtection,
isTrackingProtectionEnabledPerWebsite = isTrackingProtectionOn
)
)
}
}
data class TrackingProtectionState(
val websiteUrl: String,
val isTrackingProtectionEnabledPerApp: Boolean,
val isTrackingProtectionEnabledPerWebsite: Boolean
) : State
sealed class TrackingProtectionAction : Action {
object Stub1 : TrackingProtectionAction()
object Stub2 : TrackingProtectionAction()
}
fun trackingProtectionReducer(
state: TrackingProtectionState,
action: TrackingProtectionAction
): TrackingProtectionState {
return when (action) {
TrackingProtectionAction.Stub1 -> state
TrackingProtectionAction.Stub2 -> state
}
}

View File

@ -14,8 +14,16 @@ import kotlinx.android.synthetic.main.quicksettings_tracking_protection.*
import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds
import org.mozilla.fenix.R
interface TrackingProtectionInteractor {
fun onReportProblemSelected(websiteUrl: String)
fun onProtectionToggled(websiteUrl: String, trackingEnabled: Boolean)
fun onProtectionSettingsSelected()
fun onTrackingProtectionShown()
}
class TrackingProtectionView(
override val containerView: ViewGroup
override val containerView: ViewGroup,
val interactor: TrackingProtectionInteractor
) : LayoutContainer {
val view: View = LayoutInflater.from(containerView.context)
@ -31,9 +39,23 @@ class TrackingProtectionView(
}
fun update(state: TrackingProtectionState) {
if (state.isVisible) {
interactor.onTrackingProtectionShown()
}
reportSiteIssueAction.setOnClickListener { interactor.onReportProblemSelected(state.websiteUrl) }
trackingProtectionAction.isVisible = !state.isTrackingProtectionEnabledPerApp
if (!state.isTrackingProtectionEnabledPerApp) {
trackingProtectionAction.setOnClickListener { interactor.onProtectionSettingsSelected() }
}
trackingProtectionSwitch.isChecked = state.isTrackingProtectionEnabledPerWebsite
trackingProtectionSwitch.isEnabled = state.isTrackingProtectionEnabledPerApp
if (state.isTrackingProtectionEnabledPerApp) {
trackingProtectionSwitch.setOnCheckedChangeListener { _, isChecked ->
interactor.onProtectionToggled(state.websiteUrl, isChecked)
}
}
}
}

View File

@ -1,63 +0,0 @@
/* 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.quicksettings
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import org.mozilla.fenix.R
class WebsiteInfoStore(
initialState: WebsiteInfoState
) : Store<WebsiteInfoState, WebsiteInfoAction>(
initialState, ::websiteInfoReducer
) {
companion object {
fun createStore(url: String, isSecured: Boolean): WebsiteInfoStore {
val (stringRes, iconRes, colorRes) = when (isSecured) {
true -> getSecuredWebsiteUiValues()
false -> getInsecureWebsiteUiValues()
}
return WebsiteInfoStore(WebsiteInfoState(url, stringRes, iconRes, colorRes))
}
}
}
data class WebsiteInfoState(
val url: String,
@StringRes val securityInfoRes: Int,
@DrawableRes val iconRes: Int,
@ColorRes val iconTintRes: Int
) : State
sealed class WebsiteInfoAction : Action {
object Stub1 : WebsiteInfoAction()
object Stub2 : WebsiteInfoAction()
}
fun websiteInfoReducer(
state: WebsiteInfoState,
action: WebsiteInfoAction
): WebsiteInfoState {
return when (action) {
WebsiteInfoAction.Stub1 -> state
WebsiteInfoAction.Stub2 -> state
}
}
private fun getSecuredWebsiteUiValues() = Triple(
R.string.quick_settings_sheet_secure_connection,
R.drawable.mozac_ic_lock,
R.color.photonGreen50
)
private fun getInsecureWebsiteUiValues() = Triple(
R.string.quick_settings_sheet_insecure_connection,
R.drawable.mozac_ic_globe,
R.color.photonRed50
)

View File

@ -25,7 +25,7 @@ class WebsiteInfoView(
.inflate(R.layout.quicksettings_website_info, containerView, true)
fun update(state: WebsiteInfoState) {
bindUrl(state.url)
bindUrl(state.websiteUrl)
bindSecurityInfo(state.securityInfoRes, state.iconRes, state.iconTintRes)
}

View File

@ -1,91 +0,0 @@
/* 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.quicksettings
import android.content.Context
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.utils.Settings
class WebsitePermissionsStore(
initialState: WebsitePermissionsState
) : Store<WebsitePermissionsState, WebsitePermissionAction>(
initialState, ::reducer
) {
companion object {
fun createStore(
context: Context,
permissions: SitePermissions?,
settings: Settings
) = WebsitePermissionsStore(
WebsitePermissionsState(
camera = initWebsitePermission(context, PhoneFeature.CAMERA, permissions, settings),
microphone = initWebsitePermission(context, PhoneFeature.MICROPHONE, permissions, settings),
notification = initWebsitePermission(context, PhoneFeature.NOTIFICATION, permissions, settings),
location = initWebsitePermission(context, PhoneFeature.LOCATION, permissions, settings)
)
)
private fun initWebsitePermission(
context: Context,
phoneFeature: PhoneFeature,
permissions: SitePermissions?,
settings: Settings
): WebsitePermission {
val shouldBeVisible = phoneFeature.shouldBeVisible(permissions, settings)
return WebsitePermission(
name = phoneFeature.name,
status = phoneFeature.getActionLabel(context, permissions, settings),
visible = shouldBeVisible,
enabled = shouldBeVisible &&
phoneFeature.isAndroidPermissionGranted(context) &&
!phoneFeature.isUserPermissionGranted(permissions, settings)
)
}
private fun PhoneFeature.shouldBeVisible(
sitePermissions: SitePermissions?,
settings: Settings
) = getStatus(sitePermissions, settings) != SitePermissions.Status.NO_DECISION
private fun PhoneFeature.isUserPermissionGranted(
sitePermissions: SitePermissions?,
settings: Settings
) = getStatus(sitePermissions, settings) == SitePermissions.Status.BLOCKED
}
}
data class WebsitePermissionsState(
val camera: WebsitePermission,
val microphone: WebsitePermission,
val notification: WebsitePermission,
val location: WebsitePermission
) : State
sealed class WebsitePermissionAction : Action {
object Stub1 : WebsitePermissionAction()
object Stub2 : WebsitePermissionAction()
}
data class WebsitePermission(
val name: String,
val status: String,
val visible: Boolean,
val enabled: Boolean
)
fun reducer(
state: WebsitePermissionsState,
action: WebsitePermissionAction
): WebsitePermissionsState {
return when (action) {
WebsitePermissionAction.Stub1 -> state
WebsitePermissionAction.Stub2 -> state
}
}

View File

@ -12,8 +12,14 @@ import androidx.core.view.isVisible
import kotlinx.android.extensions.LayoutContainer
import org.mozilla.fenix.R
interface WebsitePermissionInteractor {
fun onPermissionsShown()
fun onPermissionToggled(permissionState: WebsitePermission)
}
class WebsitePermissionsView(
override val containerView: ViewGroup
override val containerView: ViewGroup,
val interactor: WebsitePermissionInteractor
) : LayoutContainer {
private val context = containerView.context
@ -21,22 +27,29 @@ class WebsitePermissionsView(
.inflate(R.layout.quicksettings_permissions, containerView, true)
fun update(state: WebsitePermissionsState) {
if (state.isVisible) {
interactor.onPermissionsShown()
}
// If more permissions are added into this View we can display them into a list
// and also use DiffUtil to only update one item in case of a permission change
bindPermission(state.camera,
Pair(view.findViewById(R.id.cameraIcon), view.findViewById(R.id.cameraActionLabel)))
Pair(view.findViewById(R.id.cameraLabel), view.findViewById(R.id.camerStatus)))
bindPermission(state.location,
Pair(view.findViewById(R.id.locationIcon), view.findViewById(R.id.locationActionLabel)))
Pair(view.findViewById(R.id.locationLabel), view.findViewById(R.id.locationStatus)))
bindPermission(state.microphone,
Pair(view.findViewById(R.id.microphoneIcon), view.findViewById(R.id.microphoneActionLabel)))
Pair(view.findViewById(R.id.microphoneLabel), view.findViewById(R.id.microphoneStatus)))
bindPermission(state.notification,
Pair(view.findViewById(R.id.notificationIcon), view.findViewById(R.id.notificationActionLabel)))
Pair(view.findViewById(R.id.notificationLabel), view.findViewById(R.id.notificationStatus)))
}
private fun bindPermission(permissionState: WebsitePermission, permissionViews: Pair<TextView, TextView>) {
val (icon, status) = permissionViews
val (label, status) = permissionViews
status.text = permissionState.status
status.isEnabled = permissionState.enabled
icon.isVisible = permissionState.visible
status.isVisible = permissionState.visible
label.isEnabled = permissionState.isEnabled
label.isVisible = permissionState.isVisible
status.isVisible = permissionState.isVisible
status.setOnClickListener { interactor.onPermissionToggled(permissionState) }
}
}

View File

@ -0,0 +1,26 @@
/* 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.quicksettings.ext
import android.content.Context
import mozilla.components.feature.sitepermissions.SitePermissions
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.utils.Settings
fun PhoneFeature.shouldBeVisible(
sitePermissions: SitePermissions?,
settings: Settings
) = getStatus(sitePermissions, settings) != SitePermissions.Status.NO_DECISION
fun PhoneFeature.shouldBeEnabled(
context: Context,
sitePermissions: SitePermissions?,
settings: Settings
) = isAndroidPermissionGranted(context) && isUserPermissionGranted(sitePermissions, settings)
fun PhoneFeature.isUserPermissionGranted(
sitePermissions: SitePermissions?,
settings: Settings
) = getStatus(sitePermissions, settings) == SitePermissions.Status.ALLOWED

View File

@ -6,6 +6,7 @@
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
@ -57,12 +58,16 @@
android:id="@+id/trackingProtectionGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="trackingProtectionLayout,trackingProtectionDivider" />
android:visibility="gone"
app:constraint_referenced_ids="trackingProtectionLayout,trackingProtectionDivider"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Group
android:id="@+id/websitePermissionsGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="websitePermissionsLayout,webSitePermissionsDivider" />
android:visibility="gone"
app:constraint_referenced_ids="websitePermissionsLayout,webSitePermissionsDivider"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -12,86 +12,102 @@
android:layout_height="wrap_content">
<TextView
android:id="@+id/cameraActionLabel"
android:id="@+id/camerStatus"
style="@style/QuickSettingsText.PermissionItemEnd"
android:layout_width="wrap_content"
android:layout_height="@dimen/quicksettings_item_height"
app:layout_constraintBottom_toTopOf="@id/microphoneActionLabel"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/microphoneStatus"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/cameraIcon"
tools:text="Allowed" />
app:layout_constraintStart_toEndOf="@id/cameraLabel"
tools:text="Allowed"
tools:visibility="visible" />
<TextView
android:id="@+id/cameraIcon"
android:id="@+id/cameraLabel"
style="@style/QuickSettingsText.Icon"
android:layout_width="0dp"
android:layout_height="@dimen/quicksettings_item_height"
android:drawableStart="@drawable/ic_camera"
android:text="@string/preference_phone_feature_camera"
app:layout_constraintBottom_toTopOf="@id/microphoneIcon"
app:layout_constraintEnd_toStartOf="@id/cameraActionLabel"
app:layout_constraintStart_toStartOf="parent" />
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/microphoneLabel"
app:layout_constraintEnd_toStartOf="@id/camerStatus"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<TextView
android:id="@+id/microphoneActionLabel"
android:id="@+id/microphoneStatus"
style="@style/QuickSettingsText.PermissionItemEnd"
android:layout_width="wrap_content"
android:layout_height="@dimen/quicksettings_item_height"
app:layout_constraintBottom_toTopOf="@id/notificationActionLabel"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/notificationStatus"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/microphoneIcon"
tools:text="Blocked by Android" />
app:layout_constraintStart_toEndOf="@id/microphoneLabel"
tools:text="Blocked by Android"
tools:visibility="visible" />
<TextView
android:id="@+id/microphoneIcon"
android:id="@+id/microphoneLabel"
style="@style/QuickSettingsText.Icon"
android:layout_width="0dp"
android:layout_height="@dimen/quicksettings_item_height"
android:drawableStart="@drawable/ic_microphone"
android:text="@string/preference_phone_feature_microphone"
app:layout_constraintBottom_toTopOf="@id/notificationIcon"
app:layout_constraintEnd_toStartOf="@id/microphoneActionLabel"
app:layout_constraintStart_toStartOf="parent" />
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/notificationLabel"
app:layout_constraintEnd_toStartOf="@id/microphoneStatus"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<TextView
android:id="@+id/notificationActionLabel"
android:id="@+id/notificationStatus"
style="@style/QuickSettingsText.PermissionItemEnd"
android:layout_width="wrap_content"
android:layout_height="@dimen/quicksettings_item_height"
app:layout_constraintBottom_toTopOf="@id/locationActionLabel"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/locationStatus"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/notificationIcon"
tools:text="Blocked" />
app:layout_constraintStart_toEndOf="@id/notificationLabel"
tools:text="Blocked"
tools:visibility="visible" />
<TextView
android:id="@+id/notificationIcon"
android:id="@+id/notificationLabel"
style="@style/QuickSettingsText.Icon"
android:layout_width="0dp"
android:layout_height="@dimen/quicksettings_item_height"
android:drawableStart="@drawable/ic_notifications"
android:text="@string/preference_phone_feature_notification"
app:layout_constraintBottom_toTopOf="@id/locationIcon"
app:layout_constraintEnd_toStartOf="@id/notificationActionLabel"
app:layout_constraintStart_toStartOf="parent" />
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/locationLabel"
app:layout_constraintEnd_toStartOf="@id/notificationStatus"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<TextView
android:id="@+id/locationActionLabel"
android:id="@+id/locationStatus"
style="@style/QuickSettingsText.PermissionItemEnd"
android:layout_width="wrap_content"
android:layout_height="@dimen/quicksettings_item_height"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/locationIcon"
tools:text="Blocked" />
app:layout_constraintStart_toEndOf="@id/locationLabel"
tools:text="Blocked"
tools:visibility="visible" />
<TextView
android:id="@+id/locationIcon"
android:id="@+id/locationLabel"
style="@style/QuickSettingsText.Icon"
android:layout_width="0dp"
android:layout_height="@dimen/quicksettings_item_height"
android:drawableStart="@drawable/ic_location"
android:text="@string/preference_phone_feature_location"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/locationActionLabel"
app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintEnd_toStartOf="@id/locationStatus"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>