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" />