From d70afcaa905138f86c1f1be000bcff0bfbebe20b Mon Sep 17 00:00:00 2001 From: Mugurell Date: Wed, 16 Oct 2019 20:15:58 +0300 Subject: [PATCH] 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. --- .../quicksettings/QuickSettingsController.kt | 167 ++++++++ .../QuickSettingsFragmentStore.kt | 382 ++++++++++++++++++ .../quicksettings/QuickSettingsInteractor.kt | 34 ++ .../QuickSettingsSheetDialogFragment.kt | 112 +++-- .../quicksettings/TrackingProtectionStore.kt | 51 --- .../quicksettings/TrackingProtectionView.kt | 24 +- .../quicksettings/WebsiteInfoStore.kt | 63 --- .../settings/quicksettings/WebsiteInfoView.kt | 2 +- .../quicksettings/WebsitePermissionsStore.kt | 91 ----- .../quicksettings/WebsitePermissionsView.kt | 31 +- .../quicksettings/ext/PhoneFeatureExt.kt | 26 ++ .../fragment_quick_settings_dialog_sheet.xml | 9 +- .../res/layout/quicksettings_permissions.xml | 76 ++-- 13 files changed, 791 insertions(+), 277 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt create mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt create mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionStore.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoStore.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsStore.kt create mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/ext/PhoneFeatureExt.kt diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt new file mode 100644 index 000000000..3cb31265e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt @@ -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) { + 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 + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt new file mode 100644 index 000000000..792d7b861 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt @@ -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( + 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 + ) + ) + } + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt new file mode 100644 index 000000000..a34c611cc --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt @@ -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) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt index 0ee6595a1..e78cac523 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt @@ -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, + 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 + } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionStore.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionStore.kt deleted file mode 100644 index 6cef214b0..000000000 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionStore.kt +++ /dev/null @@ -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( - 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 - } -} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionView.kt index 9d6bf478f..c21532c29 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionView.kt @@ -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) + } + } } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoStore.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoStore.kt deleted file mode 100644 index bc16c84b1..000000000 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoStore.kt +++ /dev/null @@ -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( - 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 -) diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoView.kt index d9d7c0bcd..f0399281b 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoView.kt @@ -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) } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsStore.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsStore.kt deleted file mode 100644 index 328d7cd44..000000000 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsStore.kt +++ /dev/null @@ -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( - 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 - } -} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsView.kt index 3534e3cfa..06db57f61 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsView.kt @@ -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) { - 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) } } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ext/PhoneFeatureExt.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ext/PhoneFeatureExt.kt new file mode 100644 index 000000000..6a904e49a --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ext/PhoneFeatureExt.kt @@ -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 diff --git a/app/src/main/res/layout/fragment_quick_settings_dialog_sheet.xml b/app/src/main/res/layout/fragment_quick_settings_dialog_sheet.xml index 30ccde506..6d69ef4d7 100644 --- a/app/src/main/res/layout/fragment_quick_settings_dialog_sheet.xml +++ b/app/src/main/res/layout/fragment_quick_settings_dialog_sheet.xml @@ -6,6 +6,7 @@ @@ -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" /> + android:visibility="gone" + app:constraint_referenced_ids="websitePermissionsLayout,webSitePermissionsDivider" + tools:visibility="visible" /> diff --git a/app/src/main/res/layout/quicksettings_permissions.xml b/app/src/main/res/layout/quicksettings_permissions.xml index d4419a2fb..46ba1782a 100644 --- a/app/src/main/res/layout/quicksettings_permissions.xml +++ b/app/src/main/res/layout/quicksettings_permissions.xml @@ -12,86 +12,102 @@ android:layout_height="wrap_content"> + app:layout_constraintStart_toEndOf="@id/cameraLabel" + tools:text="Allowed" + tools:visibility="visible" /> + android:visibility="gone" + app:layout_constraintBottom_toTopOf="@id/microphoneLabel" + app:layout_constraintEnd_toStartOf="@id/camerStatus" + app:layout_constraintStart_toStartOf="parent" + tools:visibility="visible" /> + app:layout_constraintStart_toEndOf="@id/microphoneLabel" + tools:text="Blocked by Android" + tools:visibility="visible" /> + android:visibility="gone" + app:layout_constraintBottom_toTopOf="@id/notificationLabel" + app:layout_constraintEnd_toStartOf="@id/microphoneStatus" + app:layout_constraintStart_toStartOf="parent" + tools:visibility="visible" /> + app:layout_constraintStart_toEndOf="@id/notificationLabel" + tools:text="Blocked" + tools:visibility="visible" /> + android:visibility="gone" + app:layout_constraintBottom_toTopOf="@id/locationLabel" + app:layout_constraintEnd_toStartOf="@id/notificationStatus" + app:layout_constraintStart_toStartOf="parent" + tools:visibility="visible" /> + app:layout_constraintStart_toEndOf="@id/locationLabel" + tools:text="Blocked" + tools:visibility="visible" /> + app:layout_constraintEnd_toStartOf="@id/locationStatus" + app:layout_constraintStart_toStartOf="parent" + tools:visibility="visible" />