Commit 00d971e9 authored by Arturo Mejia's avatar Arturo Mejia Committed by Sebastian Kaspari
Browse files

For #16847: Allow autoplay to controlled via the toolbar.

parent 6f3665bc
......@@ -126,8 +126,6 @@ import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
import java.lang.ref.WeakReference
import mozilla.components.feature.media.fullscreen.MediaFullscreenOrientationFeature
import org.mozilla.fenix.FeatureFlags.newMediaSessionApi
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.settings.quicksettings.QuickSettingsSheetDialogFragmentDirections
/**
* Base fragment extended by [BrowserFragment].
......@@ -368,7 +366,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
}
browserToolbarView.view.display.setOnPermissionIndicatorClickedListener {
navigateToAutoplaySetting()
showQuickSettingsDialog()
}
browserToolbarView.view.display.setOnTrackingProtectionClickedListener {
......@@ -1054,7 +1052,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
}
protected abstract fun navToQuickSettingsSheet(
session: Session,
tab: SessionState,
sitePermissions: SitePermissions?
)
......@@ -1082,15 +1080,15 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
* which lets the user control tracking protection and site settings.
*/
private fun showQuickSettingsDialog() {
val session = getSessionById() ?: return
val tab = getCurrentTab() ?: return
viewLifecycleOwner.lifecycleScope.launch(Main) {
val sitePermissions: SitePermissions? = session.url.toUri().host?.let { host ->
val sitePermissions: SitePermissions? = tab.content.url.toUri().host?.let { host ->
val storage = requireComponents.core.permissionStorage
storage.findSitePermissionsBy(host)
}
view?.let {
navToQuickSettingsSheet(session, sitePermissions)
navToQuickSettingsSheet(tab, sitePermissions)
}
}
}
......@@ -1124,6 +1122,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
}
}
protected fun getCurrentTab(): SessionState? {
return requireComponents.core.store.state.findCustomTabOrSelectedTab(customTabSessionId)
}
private suspend fun bookmarkTapped(sessionUrl: String, sessionTitle: String) = withContext(IO) {
val bookmarksStorage = requireComponents.core.bookmarksStorage
val existing =
......@@ -1293,10 +1295,4 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
browserToolbarView.setScrollFlags(enabled)
}
}
private fun navigateToAutoplaySetting() {
val directions = QuickSettingsSheetDialogFragmentDirections
.actionGlobalSitePermissionsManagePhoneFeature(PhoneFeature.AUTOPLAY_AUDIBLE)
findNavController().navigate(directions)
}
}
......@@ -20,6 +20,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.thumbnails.BrowserThumbnails
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.feature.app.links.AppLinksUseCases
......@@ -220,16 +221,17 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
return readerViewFeature.onBackPressed() || super.onBackPressed()
}
override fun navToQuickSettingsSheet(session: Session, sitePermissions: SitePermissions?) {
override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) {
val directions =
BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment(
sessionId = session.id,
url = session.url,
title = session.title,
isSecured = session.securityInfo.secure,
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = getAppropriateLayoutGravity(),
certificateName = session.securityInfo.issuer
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights
)
nav(R.id.browserFragment, directions)
}
......
......@@ -14,6 +14,7 @@ import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker
import mozilla.components.feature.addons.migration.SupportedAddonsChecker
import mozilla.components.feature.addons.update.AddonUpdater
import mozilla.components.feature.addons.update.DefaultAddonUpdater
import mozilla.components.feature.sitepermissions.SitePermissionsStorage
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.migration.state.MigrationStore
import org.mozilla.fenix.BuildConfig
......@@ -21,6 +22,7 @@ import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.components.metrics.AppStartupTelemetry
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.ClipboardHandler
......@@ -126,6 +128,10 @@ class Components(private val context: Context) {
AddonManager(core.store, core.engine, addonCollectionProvider, addonUpdater)
}
val sitePermissionsStorage by lazyMonitored {
SitePermissionsStorage(context, context.components.core.engine)
}
val analytics by lazyMonitored { Analytics(context) }
val publicSuffixList by lazyMonitored { PublicSuffixList(context) }
val clipboardHandler by lazyMonitored { ClipboardHandler(context) }
......
......@@ -5,46 +5,33 @@
package org.mozilla.fenix.components
import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.paging.DataSource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.sitepermissions.SitePermissions.Status
import mozilla.components.feature.sitepermissions.SitePermissionsStorage
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.utils.Mockable
import kotlin.coroutines.CoroutineContext
@Mockable
class PermissionStorage(private val context: Context) {
val permissionsStorage by lazy {
SitePermissionsStorage(context, context.components.core.engine)
}
fun addSitePermissionException(
origin: String,
location: Status,
notification: Status,
microphone: Status,
camera: Status
): SitePermissions {
val sitePermissions = SitePermissions(
origin = origin,
location = location,
camera = camera,
microphone = microphone,
notification = notification,
savedAt = System.currentTimeMillis()
)
class PermissionStorage(
private val context: Context,
@VisibleForTesting internal val dispatcher: CoroutineContext = Dispatchers.IO,
@VisibleForTesting internal val permissionsStorage: SitePermissionsStorage =
context.components.sitePermissionsStorage
) {
suspend fun add(sitePermissions: SitePermissions) = withContext(dispatcher) {
permissionsStorage.save(sitePermissions)
return sitePermissions
}
suspend fun findSitePermissionsBy(origin: String): SitePermissions? = withContext(Dispatchers.IO) {
suspend fun findSitePermissionsBy(origin: String): SitePermissions? = withContext(dispatcher) {
permissionsStorage.findSitePermissionsBy(origin)
}
suspend fun updateSitePermissions(sitePermissions: SitePermissions) = withContext(Dispatchers.IO) {
suspend fun updateSitePermissions(sitePermissions: SitePermissions) = withContext(dispatcher) {
permissionsStorage.update(sitePermissions)
}
......@@ -52,11 +39,11 @@ class PermissionStorage(private val context: Context) {
return permissionsStorage.getSitePermissionsPaged()
}
suspend fun deleteSitePermissions(sitePermissions: SitePermissions) = withContext(Dispatchers.IO) {
suspend fun deleteSitePermissions(sitePermissions: SitePermissions) = withContext(dispatcher) {
permissionsStorage.remove(sitePermissions)
}
suspend fun deleteAllSitePermissions() = withContext(Dispatchers.IO) {
suspend fun deleteAllSitePermissions() = withContext(dispatcher) {
permissionsStorage.removeAll()
}
}
......@@ -15,6 +15,7 @@ import kotlinx.android.synthetic.main.component_browser_top_toolbar.*
import kotlinx.android.synthetic.main.fragment_browser.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.manifest.WebAppManifestParser
import mozilla.components.concept.engine.manifest.getOrNull
import mozilla.components.feature.contextmenu.ContextMenuCandidate
......@@ -34,7 +35,6 @@ import org.mozilla.fenix.browser.CustomTabContextMenuCandidate
import org.mozilla.fenix.browser.FenixSnackbarDelegate
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
......@@ -181,16 +181,17 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
return customTabsIntegration.onBackPressed() || super.removeSessionIfNeeded()
}
override fun navToQuickSettingsSheet(session: Session, sitePermissions: SitePermissions?) {
override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) {
val directions = ExternalAppBrowserFragmentDirections
.actionGlobalQuickSettingsSheetDialogFragment(
sessionId = session.id,
url = session.url,
title = session.title,
isSecured = session.securityInfo.secure,
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = getAppropriateLayoutGravity(),
certificateName = session.securityInfo.issuer
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights
)
nav(R.id.externalAppBrowserFragment, directions)
}
......
......@@ -19,23 +19,28 @@ fun SitePermissions.toggle(featurePhone: PhoneFeature): SitePermissions {
}
fun SitePermissions.get(field: PhoneFeature) = when (field) {
PhoneFeature.AUTOPLAY ->
throw IllegalAccessException("AUTOPLAY can't be accessed via get try " +
"using AUTOPLAY_AUDIBLE and AUTOPLAY_INAUDIBLE")
PhoneFeature.CAMERA -> camera
PhoneFeature.LOCATION -> location
PhoneFeature.MICROPHONE -> microphone
PhoneFeature.NOTIFICATION -> notification
PhoneFeature.AUTOPLAY_AUDIBLE -> autoplayAudible
PhoneFeature.AUTOPLAY_INAUDIBLE -> autoplayInaudible
PhoneFeature.AUTOPLAY_AUDIBLE -> autoplayAudible.toStatus()
PhoneFeature.AUTOPLAY_INAUDIBLE -> autoplayInaudible.toStatus()
PhoneFeature.PERSISTENT_STORAGE -> localStorage
PhoneFeature.MEDIA_KEY_SYSTEM_ACCESS -> mediaKeySystemAccess
}
fun SitePermissions.update(field: PhoneFeature, value: SitePermissions.Status) = when (field) {
PhoneFeature.AUTOPLAY -> throw IllegalAccessException("AUTOPLAY can't be accessed via update " +
"try using AUTOPLAY_AUDIBLE and AUTOPLAY_INAUDIBLE")
PhoneFeature.CAMERA -> copy(camera = value)
PhoneFeature.LOCATION -> copy(location = value)
PhoneFeature.MICROPHONE -> copy(microphone = value)
PhoneFeature.NOTIFICATION -> copy(notification = value)
PhoneFeature.AUTOPLAY_AUDIBLE -> copy(autoplayAudible = value)
PhoneFeature.AUTOPLAY_INAUDIBLE -> copy(autoplayInaudible = value)
PhoneFeature.AUTOPLAY_AUDIBLE -> copy(autoplayAudible = value.toAutoplayStatus())
PhoneFeature.AUTOPLAY_INAUDIBLE -> copy(autoplayInaudible = value.toAutoplayStatus())
PhoneFeature.PERSISTENT_STORAGE -> copy(localStorage = value)
PhoneFeature.MEDIA_KEY_SYSTEM_ACCESS -> copy(mediaKeySystemAccess = value)
}
......
......@@ -29,6 +29,7 @@ enum class PhoneFeature(val androidPermissionsList: Array<String>) : Parcelable
LOCATION(arrayOf(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION)),
MICROPHONE(arrayOf(RECORD_AUDIO)),
NOTIFICATION(emptyArray()),
AUTOPLAY(emptyArray()),
AUTOPLAY_AUDIBLE(emptyArray()),
AUTOPLAY_INAUDIBLE(emptyArray()),
PERSISTENT_STORAGE(emptyArray()),
......@@ -82,7 +83,8 @@ enum class PhoneFeature(val androidPermissionsList: Array<String>) : Parcelable
NOTIFICATION -> context.getString(R.string.preference_phone_feature_notification)
PERSISTENT_STORAGE -> context.getString(R.string.preference_phone_feature_persistent_storage)
MEDIA_KEY_SYSTEM_ACCESS -> context.getString(R.string.preference_phone_feature_media_key_system_access)
AUTOPLAY_AUDIBLE, AUTOPLAY_INAUDIBLE -> context.getString(R.string.preference_browser_feature_autoplay)
AUTOPLAY, AUTOPLAY_AUDIBLE, AUTOPLAY_INAUDIBLE ->
context.getString(R.string.preference_browser_feature_autoplay)
}
}
......@@ -97,6 +99,7 @@ enum class PhoneFeature(val androidPermissionsList: Array<String>) : Parcelable
LOCATION -> R.string.pref_key_phone_feature_location
MICROPHONE -> R.string.pref_key_phone_feature_microphone
NOTIFICATION -> R.string.pref_key_phone_feature_notification
AUTOPLAY -> R.string.pref_key_browser_feature_autoplay_audible
AUTOPLAY_AUDIBLE -> R.string.pref_key_browser_feature_autoplay_audible
AUTOPLAY_INAUDIBLE -> R.string.pref_key_browser_feature_autoplay_inaudible
PERSISTENT_STORAGE -> R.string.pref_key_browser_feature_persistent_storage
......
......@@ -6,6 +6,7 @@ package org.mozilla.fenix.settings.quicksettings
import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.core.net.toUri
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
......@@ -39,6 +40,13 @@ interface QuickSettingsController {
*/
fun handlePermissionToggled(permission: WebsitePermission)
/**
* Handles change a [WebsitePermission.Autoplay].
*
* @param autoplayValue [AutoplayValue] needing to be changed.
*/
fun handleAutoplayChanged(autoplayValue: AutoplayValue)
/**
* Handles a certain set of Android permissions being explicitly granted by the user.
*
......@@ -72,8 +80,10 @@ class DefaultQuickSettingsController(
private val quickSettingsStore: QuickSettingsFragmentStore,
private val ioScope: CoroutineScope,
private val navController: NavController,
private val session: Session?,
private var sitePermissions: SitePermissions?,
@VisibleForTesting
internal val session: Session?,
@VisibleForTesting
internal var sitePermissions: SitePermissions?,
private val settings: Settings,
private val permissionStorage: PermissionStorage,
private val reload: ReloadUrlUseCase,
......@@ -122,6 +132,27 @@ class DefaultQuickSettingsController(
)
}
override fun handleAutoplayChanged(autoplayValue: AutoplayValue) {
val permissions = sitePermissions
sitePermissions = if (permissions == null) {
val origin = requireNotNull(session?.url?.toUri()?.host) {
"An origin is required to change a autoplay settings from the door hanger"
}
val sitePermissions =
autoplayValue.createSitePermissionsFromCustomRules(origin, settings)
handleAutoplayAdd(sitePermissions)
sitePermissions
} else {
val newPermission = autoplayValue.updateSitePermissions(permissions)
handlePermissionsChange(autoplayValue.updateSitePermissions(newPermission))
newPermission
}
quickSettingsStore.dispatch(
WebsitePermissionAction.ChangeAutoplay(autoplayValue)
)
}
/**
* Request a certain set of runtime Android permissions.
*
......@@ -148,6 +179,14 @@ class DefaultQuickSettingsController(
}
}
@VisibleForTesting
internal fun handleAutoplayAdd(sitePermissions: SitePermissions) {
ioScope.launch {
permissionStorage.add(sitePermissions)
reload(session)
}
}
/**
* Navigate to toggle [SitePermissions] for the specified [PhoneFeature]
*
......
......@@ -20,7 +20,7 @@ sealed class WebsiteInfoAction : QuickSettingsFragmentAction()
/**
* All possible [WebsitePermissionsState] changes as result of user / system interactions.
*/
sealed class WebsitePermissionAction : QuickSettingsFragmentAction() {
sealed class WebsitePermissionAction(open val updatedFeature: PhoneFeature) : QuickSettingsFragmentAction() {
/**
* Change resulting from toggling a specific [WebsitePermission] for the current website.
*
......@@ -31,8 +31,18 @@ sealed class WebsitePermissionAction : QuickSettingsFragmentAction() {
* @param updatedEnabledStatus [Boolean] the new [WebsitePermission#enabled] which will be shown to the user.
*/
class TogglePermission(
val updatedFeature: PhoneFeature,
override val updatedFeature: PhoneFeature,
val updatedStatus: String,
val updatedEnabledStatus: Boolean
) : WebsitePermissionAction()
) : WebsitePermissionAction(updatedFeature)
/**
* Change resulting from changing a specific [WebsitePermission.Autoplay] for the current website.
*
* @param autoplayValue [AutoplayValue] backing a certain [WebsitePermission.Autoplay].
* Allows to easily identify which permission changed
*/
class ChangeAutoplay(
val autoplayValue: AutoplayValue
) : WebsitePermissionAction(PhoneFeature.AUTOPLAY)
}
......@@ -35,16 +35,26 @@ object WebsitePermissionsStateReducer {
state: WebsitePermissionsState,
action: WebsitePermissionAction
): WebsitePermissionsState {
val key = action.updatedFeature
val value = state.getValue(key)
return when (action) {
is WebsitePermissionAction.TogglePermission -> {
val key = action.updatedFeature
val newWebsitePermission = state.getValue(key).copy(
val toggleable = value as WebsitePermission.Toggleable
val newWebsitePermission = toggleable.copy(
status = action.updatedStatus,
isEnabled = action.updatedEnabledStatus
)
state + Pair(key, newWebsitePermission)
}
is WebsitePermissionAction.ChangeAutoplay -> {
val autoplay = value as WebsitePermission.Autoplay
val newWebsitePermission = autoplay.copy(
autoplayValue = action.autoplayValue
)
state + Pair(key, newWebsitePermission)
}
}
}
}
......@@ -4,12 +4,18 @@
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.feature.sitepermissions.SitePermissions.AutoplayStatus
import mozilla.components.feature.sitepermissions.SitePermissionsRules
import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayAction
import mozilla.components.lib.state.State
import org.mozilla.fenix.R
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.utils.Settings
/**
* [State] containing all data displayed to the user by this Fragment.
......@@ -70,10 +76,192 @@ typealias WebsitePermissionsState = Map<PhoneFeature, WebsitePermission>
* @property isBlockedByAndroid Whether the corresponding *dangerous* Android permission is granted
* for the app by the user or not.
*/
data class WebsitePermission(
val phoneFeature: PhoneFeature,
val status: String,
val isVisible: Boolean,
val isEnabled: Boolean,
val isBlockedByAndroid: Boolean
)
sealed class WebsitePermission(
open val phoneFeature: PhoneFeature,
open val status: String,
open val isVisible: Boolean,
open val isEnabled: Boolean,
open val isBlockedByAndroid: Boolean
) {
data class Autoplay(
val autoplayValue: AutoplayValue,
val options: List<AutoplayValue>,
override val isVisible: Boolean
) : WebsitePermission(
PhoneFeature.AUTOPLAY,
autoplayValue.label,
isVisible,
autoplayValue.isEnabled,
isBlockedByAndroid = false
)
data class Toggleable(
override val phoneFeature: PhoneFeature,
override val status: String,
override val isVisible: Boolean,
override val isEnabled: Boolean,
override val isBlockedByAndroid: Boolean
) : WebsitePermission(
phoneFeature,
status,
isVisible,
isEnabled,
isBlockedByAndroid
)
}
sealed class AutoplayValue(
open val label: String,
open val rules: SitePermissionsRules,
open val sitePermission: SitePermissions?
) {
override fun toString() = label
abstract fun isSelected(): Boolean
abstract fun createSitePermissionsFromCustomRules(origin: String, settings: Settings): SitePermissions
abstract fun updateSitePermissions(sitePermissions: SitePermissions): SitePermissions
abstract val isEnabled: Boolean
val isVisible: Boolean get() = isSelected()
data class AllowAll(
override val label: String,
override val rules: SitePermissionsRules,
override val sitePermission: SitePermissions?
) : AutoplayValue(label, rules, sitePermission) {
override val isEnabled: Boolean = true
override fun toString() = super.toString()
override fun isSelected(): Boolean {
val actions = if (sitePermission !== null) {
listOf(
sitePermission.autoplayAudible,
sitePermission.autoplayInaudible
)
} else {
listOf(rules.autoplayAudible.toAutoplayStatus(), rules.autoplayInaudible.toAutoplayStatus())
}
return actions.all { it == AutoplayStatus.ALLOWED }
}
override fun createSitePermissionsFromCustomRules(origin: String, settings: Settings): SitePermissions {
val rules = settings.getSitePermissionsCustomSettingsRules()
return rules.copy(
autoplayAudible = AutoplayAction.ALLOWED,
autoplayInaudible = AutoplayAction.ALLOWED
).toSitePermissions(origin)
}
override fun updateSitePermissions(sitePermissions: SitePermissions): SitePermissions {
return sitePermissions.copy(
autoplayAudible = AutoplayStatus.ALLOWED,
autoplayInaudible = AutoplayStatus.ALLOWED
)
}
}
data class BlockAll(
override val label: String,
override val rules: SitePermissionsRules,
override val sitePermission: SitePermissions?
) : AutoplayValue(label, rules, sitePermission) {
override val isEnabled: Boolean = false
override fun toString() = super.toString()
override fun isSelected(): Boolean {
val actions = if (sitePermission !== null) {
listOf(
sitePermission.autoplayAudible,
sitePermission.autoplayInaudible
)
} else {
listOf(rules.autoplayAudible.toAutoplayStatus(), rules.autoplayInaudible.toAutoplayStatus())
}
return actions.all { it == AutoplayStatus.BLOCKED }
}
override fun createSitePermissionsFromCustomRules(origin: String, settings: Settings): SitePermissions {
val rules = settings.getSitePermissionsCustomSettingsRules()
return rules.copy(
autoplayAudible = AutoplayAction.BLOCKED,
autoplayInaudible = AutoplayAction.BLOCKED
).toSitePermissions(origin)