1
0
Fork 0
fenix/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt

503 lines
19 KiB
Kotlin

/* 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 androidx.annotation.VisibleForTesting
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.Reducer
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import org.mozilla.fenix.R
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.settings.quicksettings.QuickSettingsFragmentStore.Companion.createStore
import org.mozilla.fenix.settings.quicksettings.ext.shouldBeEnabled
import org.mozilla.fenix.settings.quicksettings.ext.shouldBeVisible
import org.mozilla.fenix.utils.Settings
/**
* [QuickSettingsSheetDialogFragment]'s unique [Store].
* Encompasses it's own:
* - [State] for all Views displayed in this Fragment.
* - [Action]s mapping a user / system interaction to an intention to modify the above State.
* - [Reducer]s for modifying the above State based on the above Actions.
*
* The [createStore] helper method can be used for creating one such [State] based on all current
* conditions of the app and web page visited.
*
* @param initialState [QuickSettingsFragmentState] that will be shown initially to the user.
*/
class QuickSettingsFragmentStore(
initialState: QuickSettingsFragmentState
) : Store<QuickSettingsFragmentState, QuickSettingsFragmentAction>(
initialState,
::quickSettingsFragmentReducer
) {
companion object {
/**
* String, Drawable & Drawable Tint color used to display that the current website connection is secured.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
val getSecuredWebsiteUiValues = Triple(
R.string.quick_settings_sheet_secure_connection,
R.drawable.mozac_ic_lock,
R.color.photonGreen50
)
/**
* String, Drawable & Drawable Tint color used to display that the current website connection is
* **not** secured.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
val getInsecureWebsiteUiValues = Triple(
R.string.quick_settings_sheet_insecure_connection,
R.drawable.mozac_ic_globe,
R.color.photonRed50
)
/**
* Construct an initial [QuickSettingsFragmentState] for all Views displayed by the
* [QuickSettingsSheetDialogFragment].
*
* @param context [Context] used for access to various Android resources.
* @param websiteUrl [String] the URL of the current web page.
* @param isSecured [Boolean] whether the connection is secured (TLS) or not.
* @param permissions [SitePermissions]? list of website permissions and their status.
* @param settings [Settings] application settings.
*/
@Suppress("LongParameterList")
fun createStore(
context: Context,
websiteUrl: String,
isSecured: Boolean,
permissions: SitePermissions?,
settings: Settings
) = QuickSettingsFragmentStore(
QuickSettingsFragmentState(
webInfoState = createWebsiteInfoState(websiteUrl, isSecured),
websitePermissionsState = createWebsitePermissionState(
context,
permissions,
settings
)
)
)
/**
* Construct an initial [WebsiteInfoState] to be rendered by [WebsiteInfoView]
* based on the current website's status and connection.
*
* While being displayed users have no way of modifying it.
*
* @param websiteUrl [String] the URL of the current web page.
* @param isSecured [Boolean] whether the connection is secured (TLS) or not.
*/
@VisibleForTesting
fun createWebsiteInfoState(
websiteUrl: String,
isSecured: Boolean
): WebsiteInfoState {
val (stringRes, iconRes, colorRes) = when (isSecured) {
true -> getSecuredWebsiteUiValues
false -> getInsecureWebsiteUiValues
}
return WebsiteInfoState(websiteUrl, stringRes, iconRes, colorRes)
}
/**
* Construct an initial [WebsitePermissionsState] to be rendered by [WebsitePermissionsView]
* containing the permissions requested by the current website.
*
* Users can modify the returned [WebsitePermissionsState] after it is initially displayed.
*
* @param context [Context] used for various Android interactions.
* @param permissions [SitePermissions]? list of website permissions and their status.
* @param settings [Settings] application settings.
*/
@VisibleForTesting
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
)
}
/**
* [PhoneFeature] to a [WebsitePermission] mapper.
*/
@VisibleForTesting(otherwise = VisibleForTesting.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
}
}
/**
* Helper method for getting the [WebsitePermission] properties based on a specific [PhoneFeature].
*/
@VisibleForTesting(otherwise = VisibleForTesting.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)
)
/**
* Helper class acting as a temporary container of [WebsitePermission] properties.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
data class PermissionStatus(
val status: String,
val isVisible: Boolean,
val isEnabled: Boolean,
val isBlockedByAndroid: Boolean
)
}
}
// -------------------------------------------------------------------------------------------------
// States
// -------------------------------------------------------------------------------------------------
/**
* [State] containing all data displayed to the user by this Fragment.
*
* Partitioned further to contain mutiple states for each standalone View this Fragment holds.
*/
data class QuickSettingsFragmentState(
val webInfoState: WebsiteInfoState,
val websitePermissionsState: WebsitePermissionsState
) : State
/**
* [State] to be rendered by [WebsiteInfoView] indicating whether the connection is secure or not.
*
* @param websiteUrl [String] the URL of the current web page.
* @param securityInfoRes [StringRes] for the connection description.
* @param iconRes [DrawableRes] image indicating the connection status.
* @param iconTintRes [ColorRes] icon color.
*/
data class WebsiteInfoState(
val websiteUrl: String,
@StringRes val securityInfoRes: Int,
@DrawableRes val iconRes: Int,
@ColorRes val iconTintRes: Int
) : State
/**
* /**
* [State] to be rendered by [WebsitePermissionsView] displaying all explicitly allowed or blocked
* website permissions.
*
* @param isVisible [Boolean] whether this contains data that needs to be displayed to the user.
* @param camera [WebsitePermission] containing all information about the *camera* permission.
* @param microphone [WebsitePermission] containing all information about the *microphone* permission.
* @param notification [notification] containing all information about the *notification* permission.
* @param location [WebsitePermission] containing all information about the *location* permission.
*/
*/
data class WebsitePermissionsState(
val isVisible: Boolean,
val camera: WebsitePermission,
val microphone: WebsitePermission,
val notification: WebsitePermission,
val location: WebsitePermission
) : State
/**
* Wrapper over a website permission encompassing all it's needed state to be rendered on the screen.
*
* Contains a limited number of implementations because there is a known, finite number of permissions
* we need to display to the user.
*/
sealed class WebsitePermission {
/**
* The *allowed* / *blocked* permission status to be shown to the user.
*/
abstract val status: String
/**
* Whether this permission should be shown to the user.
*/
abstract val isVisible: Boolean
/**
* Visual indication about whether this permission is *enabled* / *disabled*
*/
abstract val isEnabled: Boolean
/**
* Whether the corresponding *dangerous* Android permission is granted for the app by the user or not.
*/
abstract val isBlockedByAndroid: Boolean
/**
* Helper method mimicking the default generated *copy()* method for a data class.
* Allows us using a familiar API in the reducer.
*/
abstract fun copy(
status: String = this.status,
isVisible: Boolean = this.isVisible,
isEnabled: Boolean = this.isEnabled,
isBlockedByAndroid: Boolean = this.isBlockedByAndroid
): WebsitePermission
/**
* Contains all information about the *camera* permission.
*/
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
)
}
/**
* Contains all information about the *microphone* permission.
*/
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
)
}
/**
* Contains all information about the *notification* permission.
*/
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
)
}
/**
* Contains all information about the *location* permission.
*/
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
// -------------------------------------------------------------------------------------------------
/**
* Parent [Action] for all the [QuickSettingsFragmentState] changes.
*/
sealed class QuickSettingsFragmentAction : Action
/**
* All possible [WebsiteInfoState] changes as result of user / system interactions.
*/
sealed class WebsiteInfoAction : QuickSettingsFragmentAction()
/**
* All possible [WebsitePermissionsState] changes as result of user / system interactions.
*/
sealed class WebsitePermissionAction : QuickSettingsFragmentAction() {
/**
* Change resulting from toggling a specific [WebsitePermission] for the current website.
*
* @param updatedFeature [PhoneFeature] backing a certain [WebsitePermission].
* Allows to easily identify which permission changed
* **Must be the name of one of the properties of [WebsitePermissionsState]**.
* @param updatedStatus [String] the new [WebsitePermission#status] which will be shown to the user.
* @param updatedEnabledStatus [Boolean] the new [WebsitePermission#enabled] which will be shown to the user.
*/
class TogglePermission(
val websitePermission: WebsitePermission,
val updatedStatus: String,
val updatedEnabledStatus: Boolean
) : WebsitePermissionAction()
}
// -------------------------------------------------------------------------------------------------
// Reducers
// -------------------------------------------------------------------------------------------------
/**
* Parent [Reducer] for all [QuickSettingsFragmentState]s of all Views shown in this Fragment.
*/
fun quickSettingsFragmentReducer(
state: QuickSettingsFragmentState,
action: QuickSettingsFragmentAction
): QuickSettingsFragmentState {
return when (action) {
is WebsiteInfoAction -> state.copy(
webInfoState = WebsiteInfoStateReducer.reduce(
state.webInfoState,
action
)
)
is WebsitePermissionAction -> state.copy(
websitePermissionsState = WebsitePermissionsStateReducer.reduce(
state.websitePermissionsState,
action
)
)
}
}
@Suppress("UNUSED_PARAMETER") // the action paramater is unused
object WebsiteInfoStateReducer {
/**
* Handles creating a new [WebsiteInfoState] based on the specific [WebsiteInfoAction]
*/
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 {
/**
* Handles creating a new [WebsitePermissionsState] based on the specific [WebsitePermissionAction]
*/
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
)
)
}
}
}
}
}