diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/LibraryMenuTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/LibraryMenuTest.kt index 4f06cb24b..3a761a58f 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/LibraryMenuTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/LibraryMenuTest.kt @@ -3,6 +3,7 @@ package org.mozilla.fenix.ui import okhttp3.mockwebserver.MockWebServer import org.junit.After import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.mozilla.fenix.helpers.AndroidAssetDispatcher @@ -36,6 +37,7 @@ class LibraryMenuTest { } @Test + @Ignore("Intermittently failing. See https://github.com/mozilla-mobile/fenix/issues/9287") fun libraryMenuItemsTest() { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { @@ -50,6 +52,7 @@ class LibraryMenuTest { } @Test + @Ignore("Intermittent failure. See https://github.com/mozilla-mobile/fenix/issues/9232") fun backButtonTest() { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { diff --git a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt index ee56f6327..1b199d927 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -391,6 +391,13 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session view = view ) + context.settings().setSitePermissionSettingListener(viewLifecycleOwner) { + // If the user connects to WIFI while on the BrowserFragment, this will update the + // SitePermissionsRules (specifically autoplay) accordingly + this.context?.let { assignSitePermissionsRules(it) } + } + assignSitePermissionsRules(context) + fullScreenFeature.set( feature = FullScreenFeature( sessionManager, @@ -541,8 +548,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session components.useCases.sessionUseCases.reload() } hideToolbar() - - assignSitePermissionsRules() } @CallSuper @@ -673,8 +678,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session /** * Updates the site permissions rules based on user settings. */ - private fun assignSitePermissionsRules() { - val settings = requireContext().settings() + private fun assignSitePermissionsRules(context: Context) { + val settings = context.settings() val rules: SitePermissionsRules = settings.getSitePermissionsCustomSettingsRules() diff --git a/app/src/main/java/org/mozilla/fenix/components/Components.kt b/app/src/main/java/org/mozilla/fenix/components/Components.kt index 091405ed3..4aaf6e570 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -4,6 +4,7 @@ package org.mozilla.fenix.components +import android.app.Application import android.content.Context import android.content.Intent import androidx.core.net.toUri @@ -17,8 +18,11 @@ import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.lib.publicsuffixlist.PublicSuffixList import mozilla.components.support.migration.state.MigrationStore import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.ext.settings import org.mozilla.fenix.test.Mockable import org.mozilla.fenix.utils.ClipboardHandler +import org.mozilla.fenix.wifi.WifiConnectionMonitor +import org.mozilla.fenix.wifi.WifiIntegration import java.util.concurrent.TimeUnit private const val DAY_IN_MINUTES = 24 * 60L @@ -96,4 +100,12 @@ class Components(private val context: Context) { val migrationStore by lazy { MigrationStore() } val performance by lazy { PerformanceComponent() } val push by lazy { Push(context, analytics.crashReporter) } + val wifiIntegration by lazy { + WifiIntegration( + settings = context.settings(), + wifiConnectionMonitor = WifiConnectionMonitor( + context as Application + ) + ) + } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/PhoneFeature.kt b/app/src/main/java/org/mozilla/fenix/settings/PhoneFeature.kt index 8db4a3be6..dfd1ac46a 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/PhoneFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/PhoneFeature.kt @@ -14,6 +14,10 @@ import mozilla.components.feature.sitepermissions.SitePermissionsRules import mozilla.components.support.ktx.android.content.isPermissionGranted import org.mozilla.fenix.R import org.mozilla.fenix.ext.getPreferenceKey +import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_ALLOW_ALL +import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_ALLOW_ON_WIFI +import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_ALL +import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_AUDIBLE import org.mozilla.fenix.utils.Settings import android.Manifest.permission.CAMERA as CAMERA_PERMISSION @@ -49,11 +53,13 @@ enum class PhoneFeature(val id: Int, val androidPermissionsList: Array) when (isAndroidPermissionGranted(context)) { false -> R.string.phone_feature_blocked_by_android else -> when (this) { - AUTOPLAY_AUDIBLE, AUTOPLAY_INAUDIBLE -> { - when (getStatus(sitePermissions, settings)) { - SitePermissions.Status.BLOCKED -> R.string.preference_option_autoplay_blocked - SitePermissions.Status.ALLOWED -> R.string.preference_option_autoplay_allowed - else -> R.string.preference_option_autoplay_allowed + AUTOPLAY_AUDIBLE -> { + when (settings?.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) ?: AUTOPLAY_BLOCK_ALL) { + AUTOPLAY_ALLOW_ALL -> R.string.preference_option_autoplay_allowed2 + AUTOPLAY_ALLOW_ON_WIFI -> R.string.preference_option_autoplay_allowed_wifi_only2 + AUTOPLAY_BLOCK_AUDIBLE -> R.string.preference_option_autoplay_block_audio2 + AUTOPLAY_BLOCK_ALL -> R.string.preference_option_autoplay_blocked3 + else -> R.string.preference_option_autoplay_blocked3 } } else -> { diff --git a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsFragment.kt index 4176e04c1..2458987aa 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsFragment.kt @@ -61,10 +61,17 @@ class SitePermissionsFragment : PreferenceFragmentCompat() { val settings = context.settings() val summary = phoneFeature.getActionLabel(context, settings = settings) + // Remove autoplaySummary after https://bugzilla.mozilla.org/show_bug.cgi?id=1621825 is fixed + val autoplaySummary = + if (summary == context.getString(R.string.preference_option_autoplay_allowed2)) { + context.getString(R.string.preference_option_autoplay_allowed_wifi_only2) + } else { + null + } val preferenceKey = phoneFeature.getPreferenceKey(context) val cameraPhoneFeatures: Preference = requireNotNull(findPreference(preferenceKey)) - cameraPhoneFeatures.summary = summary + cameraPhoneFeatures.summary = autoplaySummary ?: summary cameraPhoneFeatures.onPreferenceClickListener = OnPreferenceClickListener { navigateToPhoneFeature(phoneFeature) diff --git a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManagePhoneFeatureFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManagePhoneFeatureFragment.kt index d0961eef4..83f3249f7 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManagePhoneFeatureFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManagePhoneFeatureFragment.kt @@ -4,6 +4,7 @@ package org.mozilla.fenix.settings.sitepermissions +import android.content.Context import android.content.Intent import android.graphics.Color import android.net.Uri @@ -20,18 +21,30 @@ import android.view.ViewGroup import android.widget.Button import android.widget.RadioButton import androidx.fragment.app.Fragment +import kotlinx.android.synthetic.main.fragment_manage_site_permissions_feature_phone.view.ask_to_allow_radio +import kotlinx.android.synthetic.main.fragment_manage_site_permissions_feature_phone.view.block_radio +import kotlinx.android.synthetic.main.fragment_manage_site_permissions_feature_phone.view.fourth_radio +import kotlinx.android.synthetic.main.fragment_manage_site_permissions_feature_phone.view.third_radio import mozilla.components.feature.sitepermissions.SitePermissionsRules import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.ALLOWED import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.ASK_TO_ALLOW import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.BLOCKED import org.mozilla.fenix.R +import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.settings.PhoneFeature +import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY_AUDIBLE +import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY_INAUDIBLE import org.mozilla.fenix.settings.initBlockedByAndroidView import org.mozilla.fenix.settings.setStartCheckedIndicator import org.mozilla.fenix.utils.Settings +const val AUTOPLAY_BLOCK_ALL = 0 +const val AUTOPLAY_BLOCK_AUDIBLE = 1 +const val AUTOPLAY_ALLOW_ON_WIFI = 2 +const val AUTOPLAY_ALLOW_ALL = 3 + @SuppressWarnings("TooManyFunctions") class SitePermissionsManagePhoneFeatureFragment : Fragment() { private lateinit var phoneFeature: PhoneFeature @@ -62,6 +75,8 @@ class SitePermissionsManagePhoneFeatureFragment : Fragment() { initFirstRadio(rootView) initSecondRadio(rootView) + initThirdRadio(rootView) + initFourthRadio(rootView) bindBlockedByAndroidContainer(rootView) return rootView @@ -73,66 +88,133 @@ class SitePermissionsManagePhoneFeatureFragment : Fragment() { } private fun initFirstRadio(rootView: View) { - val radio = rootView.findViewById(R.id.ask_to_allow_radio) - val askToAllowText = when (phoneFeature) { - PhoneFeature.AUTOPLAY_AUDIBLE -> - getString(R.string.preference_option_autoplay_blocked) - else -> getString(R.string.preference_option_phone_feature_ask_to_allow) + with(rootView.ask_to_allow_radio) { + if (phoneFeature == AUTOPLAY_AUDIBLE) { + // Disabled because GV does not allow this setting. TODO Reenable after + // https://bugzilla.mozilla.org/show_bug.cgi?id=1621825 is fixed +// text = getString(R.string.preference_option_autoplay_allowed2) +// setOnClickListener { +// saveActionInSettings(it.context, AUTOPLAY_ALLOW_ALL) +// } +// restoreState(AUTOPLAY_ALLOW_ALL) + visibility = View.GONE + } else { + text = getCombinedLabel( + getString(R.string.preference_option_phone_feature_ask_to_allow), + getString(R.string.phone_feature_recommended) + ) + setOnClickListener { + saveActionInSettings(ASK_TO_ALLOW) + } + restoreState(ASK_TO_ALLOW) + visibility = View.VISIBLE + } } - val recommendedText = getString(R.string.phone_feature_recommended) - val recommendedTextSize = - resources.getDimensionPixelSize(R.dimen.phone_feature_label_recommended_text_size) - val recommendedSpannable = SpannableString(recommendedText) - - recommendedSpannable.setSpan( - ForegroundColorSpan(Color.GRAY), - 0, - recommendedSpannable.length, - SPAN_EXCLUSIVE_INCLUSIVE - ) + } - recommendedSpannable.setSpan( - AbsoluteSizeSpan(recommendedTextSize), 0, - recommendedSpannable.length, - SPAN_EXCLUSIVE_INCLUSIVE - ) + private fun initSecondRadio(rootView: View) { + with(rootView.block_radio) { + if (phoneFeature == AUTOPLAY_AUDIBLE) { + text = getCombinedLabel( + getString(R.string.preference_option_autoplay_allowed_wifi_only2), + getString(R.string.preference_option_autoplay_allowed_wifi_subtext) + ) + setOnClickListener { + // TODO replace with AUTOPLAY_ALLOW_ON_WIFI when + // https://bugzilla.mozilla.org/show_bug.cgi?id=1621825 is fixed. This GV bug + // makes ALLOW_ALL behave as ALLOW_ON_WIFI + saveActionInSettings(it.context, AUTOPLAY_ALLOW_ALL) + } + // TODO replace with AUTOPLAY_ALLOW_ON_WIFI when + // https://bugzilla.mozilla.org/show_bug.cgi?id=1621825 is fixed. This GV bug + // makes ALLOW_ALL behave as ALLOW_ON_WIFI + restoreState(AUTOPLAY_ALLOW_ALL) + } else { + text = getString(R.string.preference_option_phone_feature_blocked) + setOnClickListener { + saveActionInSettings(BLOCKED) + } + restoreState(BLOCKED) + } + } + } - radio.text = with(SpannableStringBuilder()) { - append(askToAllowText) - append("\n") - append(recommendedSpannable) - this + private fun initThirdRadio(rootView: View) { + with(rootView.third_radio) { + if (phoneFeature == AUTOPLAY_AUDIBLE) { + visibility = View.VISIBLE + text = getString(R.string.preference_option_autoplay_block_audio2) + setOnClickListener { + saveActionInSettings(it.context, AUTOPLAY_BLOCK_AUDIBLE) + } + restoreState(AUTOPLAY_BLOCK_AUDIBLE) + } else { + visibility = View.GONE + } } - val expectedAction = if (phoneFeature == PhoneFeature.AUTOPLAY_AUDIBLE) BLOCKED else ASK_TO_ALLOW - radio.setOnClickListener { - saveActionInSettings(expectedAction) + } + + private fun initFourthRadio(rootView: View) { + with(rootView.fourth_radio) { + if (phoneFeature == AUTOPLAY_AUDIBLE) { + visibility = View.VISIBLE + text = getCombinedLabel( + getString(R.string.preference_option_autoplay_blocked3), + getString(R.string.phone_feature_recommended) + ) + setOnClickListener { + saveActionInSettings(it.context, AUTOPLAY_BLOCK_ALL) + } + restoreState(AUTOPLAY_BLOCK_ALL) + } else { + visibility = View.GONE + } } - radio.restoreState(expectedAction) } - private fun RadioButton.restoreState(action: SitePermissionsRules.Action) { - if (phoneFeature.getAction(settings) == action) { + private fun RadioButton.restoreState(buttonAction: SitePermissionsRules.Action) { + if (phoneFeature.getAction(settings) == buttonAction) { this.isChecked = true this.setStartCheckedIndicator() } } - private fun initSecondRadio(rootView: View) { - val radio = rootView.findViewById(R.id.block_radio) - radio.text = when (phoneFeature) { - PhoneFeature.AUTOPLAY_AUDIBLE, PhoneFeature.AUTOPLAY_INAUDIBLE -> - getString(R.string.preference_option_autoplay_allowed) - else -> getString(R.string.preference_option_phone_feature_blocked) + private fun RadioButton.restoreState(buttonAutoplaySetting: Int) { + if (settings.getAutoplayUserSetting(AUTOPLAY_BLOCK_ALL) == buttonAutoplaySetting) { + this.isChecked = true + this.setStartCheckedIndicator() } - val expectedAction = if (phoneFeature == PhoneFeature.AUTOPLAY_AUDIBLE) ALLOWED else BLOCKED - radio.setOnClickListener { - saveActionInSettings(expectedAction) + } + + private fun saveActionInSettings(action: SitePermissionsRules.Action) { + settings.setSitePermissionsPhoneFeatureAction(phoneFeature, action) + } + + /** + * Saves the user selected autoplay setting. + * + * See [Settings.setAutoplayUserSetting] kdoc for an explanation of why this cannot follow the + * same code path as other permissions. + */ + private fun saveActionInSettings(context: Context, autoplaySetting: Int) { + settings.setAutoplayUserSetting(autoplaySetting) + val (audible, inaudible) = when (autoplaySetting) { + AUTOPLAY_ALLOW_ALL -> ALLOWED to ALLOWED + AUTOPLAY_ALLOW_ON_WIFI -> { + context.components.wifiIntegration.addWifiConnectedListener() + return + } + AUTOPLAY_BLOCK_AUDIBLE -> BLOCKED to ALLOWED + AUTOPLAY_BLOCK_ALL -> BLOCKED to BLOCKED + else -> return } - radio.restoreState(expectedAction) + context.components.wifiIntegration.removeWifiConnectedListener() + settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_AUDIBLE, audible) + settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_INAUDIBLE, inaudible) } private fun bindBlockedByAndroidContainer(rootView: View) { - blockedByAndroidView = rootView.findViewById(R.id.permissions_blocked_container) + blockedByAndroidView = rootView.findViewById(R.id.permissions_blocked_container) initSettingsButton(blockedByAndroidView) } @@ -158,7 +240,32 @@ class SitePermissionsManagePhoneFeatureFragment : Fragment() { startActivity(intent) } - private fun saveActionInSettings(action: SitePermissionsRules.Action) { - settings.setSitePermissionsPhoneFeatureAction(phoneFeature, action) + /** + * Returns a [CharSequence] that arranges and styles [mainText], a line break, and then [subText] + */ + private fun getCombinedLabel(mainText: CharSequence, subText: CharSequence): CharSequence { + val subTextSize = + resources.getDimensionPixelSize(R.dimen.phone_feature_label_recommended_text_size) + val recommendedSpannable = SpannableString(subText) + + recommendedSpannable.setSpan( + ForegroundColorSpan(Color.GRAY), + 0, + recommendedSpannable.length, + SPAN_EXCLUSIVE_INCLUSIVE + ) + + recommendedSpannable.setSpan( + AbsoluteSizeSpan(subTextSize), 0, + recommendedSpannable.length, + SPAN_EXCLUSIVE_INCLUSIVE + ) + + return with(SpannableStringBuilder()) { + append(mainText) + append("\n") + append(recommendedSpannable) + this + } } } diff --git a/app/src/main/java/org/mozilla/fenix/utils/OnWifiChanged.java b/app/src/main/java/org/mozilla/fenix/utils/OnWifiChanged.java new file mode 100644 index 000000000..208b37f69 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/utils/OnWifiChanged.java @@ -0,0 +1,14 @@ +package org.mozilla.fenix.utils; + +/** + * Functional interface for listening to when wifi is/is not connected. + * + * This is not a () -> Boolean so that method parameters can be more clearly typed. + * + * This file is in Java because of the SAM conversion problem in Kotlin. + * See https://youtrack.jetbrains.com/issue/KT-7770. + */ +@FunctionalInterface +public interface OnWifiChanged { + void invoke(boolean Connected); +} diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index c6d722358..0510f67d3 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -12,6 +12,7 @@ import android.content.SharedPreferences import android.view.accessibility.AccessibilityManager import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting.PRIVATE +import androidx.lifecycle.LifecycleOwner import mozilla.components.feature.sitepermissions.SitePermissionsRules import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayAction @@ -29,8 +30,11 @@ import org.mozilla.fenix.components.metrics.MozillaProductDetector import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.settings.PhoneFeature import org.mozilla.fenix.settings.deletebrowsingdata.DeleteBrowsingDataOnQuitType +import org.mozilla.fenix.settings.registerOnSharedPreferenceChangeListener import java.security.InvalidParameterException +private const val AUTOPLAY_USER_SETTING = "AUTOPLAY_USER_SETTING" + /** * A simple wrapper for SharedPreferences that makes reading preference a little bit easier. */ @@ -175,10 +179,6 @@ class Settings private constructor( default = true ) - val isAutoPlayEnabled = getSitePermissionsPhoneFeatureAction( - PhoneFeature.AUTOPLAY_AUDIBLE, Action.BLOCKED - ) != Action.BLOCKED - private var trackingProtectionOnboardingShownThisSession = false var isOverrideTPPopupsForPerformanceTest = false @@ -452,6 +452,34 @@ class Settings private constructor( ) = preferences.getInt(feature.getPreferenceKey(appContext), default.toInt()).toAction() + /** + * Saves the user selected autoplay setting. + * + * Under the hood, autoplay is represented by two settings, [AUTOPLAY_AUDIBLE] and + * [AUTOPLAY_INAUDIBLE]. The user selection cannot be inferred from the combination of these + * settings because, while on [AUTOPLAY_ALLOW_ON_WIFI], they will be indistinguishable from + * either [AUTOPLAY_ALLOW_ALL] or [AUTOPLAY_BLOCK_ALL]. Because of this, we are forced to save + * the user selected setting as well. + */ + fun setAutoplayUserSetting( + autoplaySetting: Int + ) { + preferences.edit().putInt(AUTOPLAY_USER_SETTING, autoplaySetting).apply() + } + + /** + * Gets the user selected autoplay setting. + * + * Under the hood, autoplay is represented by two settings, [AUTOPLAY_AUDIBLE] and + * [AUTOPLAY_INAUDIBLE]. The user selection cannot be inferred from the combination of these + * settings because, while on [AUTOPLAY_ALLOW_ON_WIFI], they will be indistinguishable from + * either [AUTOPLAY_ALLOW_ALL] or [AUTOPLAY_BLOCK_ALL]. Because of this, we are forced to save + * the user selected setting as well. + */ + fun getAutoplayUserSetting( + default: Int + ) = preferences.getInt(AUTOPLAY_USER_SETTING, default) + fun getSitePermissionsPhoneFeatureAutoplayAction( feature: PhoneFeature, default: AutoplayAction = AutoplayAction.BLOCKED @@ -471,11 +499,25 @@ class Settings private constructor( location = getSitePermissionsPhoneFeatureAction(PhoneFeature.LOCATION), camera = getSitePermissionsPhoneFeatureAction(PhoneFeature.CAMERA), autoplayAudible = getSitePermissionsPhoneFeatureAutoplayAction(PhoneFeature.AUTOPLAY_AUDIBLE), - // TODO autoplayInaudible will be hardcoded until additional options are added in #8017 - autoplayInaudible = AutoplayAction.ALLOWED + autoplayInaudible = getSitePermissionsPhoneFeatureAutoplayAction(PhoneFeature.AUTOPLAY_INAUDIBLE) ) } + fun setSitePermissionSettingListener(lifecycleOwner: LifecycleOwner, listener: () -> Unit) { + val sitePermissionKeys = listOf( + PhoneFeature.NOTIFICATION, + PhoneFeature.MICROPHONE, + PhoneFeature.LOCATION, + PhoneFeature.CAMERA, + PhoneFeature.AUTOPLAY_AUDIBLE, + PhoneFeature.AUTOPLAY_INAUDIBLE + ).map { it.getPreferenceKey(appContext) } + + preferences.registerOnSharedPreferenceChangeListener(lifecycleOwner) { _, key -> + if (key in sitePermissionKeys) listener.invoke() + } + } + var shouldPromptToSaveLogins by booleanPreference( appContext.getPreferenceKey(R.string.pref_key_save_logins), default = true diff --git a/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt b/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt new file mode 100644 index 000000000..34a6e5f7a --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt @@ -0,0 +1,107 @@ +/* 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.wifi + +import android.app.Application +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest + +/** + * Attaches itself to the [Application] and listens for WIFI available/not available events. This + * allows calling code to set simpler listeners. + * + * Example: + * ```kotlin + * app.components.wifiConnectionListener.addOnWifiConnectedChangedListener { isConnected -> + * if (isConnected) { + * downloadThing() + * } + * } + * app.components.wifiConnectionListener.start() + * ``` + */ +class WifiConnectionMonitor(app: Application) { + private val callbacks = mutableSetOf<(Boolean) -> Unit>() + private val connectivityManager = app.getSystemService(Context.CONNECTIVITY_SERVICE) as + ConnectivityManager + + private var lastKnownStateWasAvailable: Boolean? = null + private var isRegistered = false + + private val frameworkListener = object : ConnectivityManager.NetworkCallback() { + override fun onLost(network: Network?) { + callbacks.forEach { it(false) } + lastKnownStateWasAvailable = false + } + + override fun onAvailable(network: Network?) { + callbacks.forEach { it(true) } + lastKnownStateWasAvailable = true + } + } + + /** + * Attaches the [WifiConnectionMonitor] to the application. After this has been called, callbacks + * added via [addOnWifiConnectedChangedListener] will be called until either the app exits, or + * [stop] is called. + * + * Any existing callbacks will be called with the current state when this is called. + */ + fun start() { + // Framework code throws if a listener is registered twice without unregistering. + if (isRegistered) return + val request = NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build() + + // AFAICT, the framework does not send an event when a new NetworkCallback is registered + // while the WIFI is not connected, so we push this manually. If the WIFI is on, it will send + // a follow up event shortly + val noCallbacksReceivedYet = lastKnownStateWasAvailable == null + if (noCallbacksReceivedYet) { + lastKnownStateWasAvailable = false + callbacks.forEach { it(false) } + } + + connectivityManager.registerNetworkCallback(request, frameworkListener) + isRegistered = true + } + + /** + * Detatches the [WifiConnectionMonitor] from the app. No callbacks added via + * [addOnWifiConnectedChangedListener] will be called after this has been called. + */ + fun stop() { + // Framework code will throw if an unregistered listener attempts to unregister. + if (!isRegistered) return + connectivityManager.unregisterNetworkCallback(frameworkListener) + isRegistered = false + } + + /** + * Adds [onWifiChanged] to a list of listeners that will be called whenever WIFI connects or + * disconnects. + * + * If [onWifiChanged] is successfully added (i.e., it is a new listener), it will be immediately + * called with the last known state. + */ + fun addOnWifiConnectedChangedListener(onWifiChanged: (Boolean) -> Unit) { + val lastKnownState = lastKnownStateWasAvailable + if (callbacks.add(onWifiChanged) && lastKnownState != null) { + onWifiChanged(lastKnownState) + } + } + + /** + * Removes [onWifiChanged] from the list of listeners to be called whenever WIFI connects or + * disconnects. + */ + fun removeOnWifiConnectedChangedListener(onWifiChanged: (Boolean) -> Unit) { + callbacks.remove(onWifiChanged) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/wifi/WifiIntegration.kt b/app/src/main/java/org/mozilla/fenix/wifi/WifiIntegration.kt new file mode 100644 index 000000000..b81df21fd --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/wifi/WifiIntegration.kt @@ -0,0 +1,57 @@ +/* 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.wifi + +import mozilla.components.feature.sitepermissions.SitePermissionsRules +import org.mozilla.fenix.settings.PhoneFeature +import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_ALLOW_ON_WIFI +import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_ALL +import org.mozilla.fenix.utils.Settings + +/** + * Handles implementation details of only setting up a WIFI connectivity listener if the current + * user settings require it. + */ +class WifiIntegration(private val settings: Settings, private val wifiConnectionMonitor: WifiConnectionMonitor) { + + /** + * Adds listener for autplay setting [AUTOPLAY_ALLOW_ON_WIFI]. Sets all autoplay to allowed when + * WIFI is connected, blocked otherwise. + */ + private val wifiConnectedListener: ((Boolean) -> Unit) by lazy { + { connected: Boolean -> + val setting = + if (connected) SitePermissionsRules.Action.ALLOWED else SitePermissionsRules.Action.BLOCKED + settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.AUTOPLAY_AUDIBLE, setting) + settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.AUTOPLAY_INAUDIBLE, setting) + } + } + + /** + * If autoplay is only enabled on WIFI, sets a WIFI listener to set them accordingly. Otherwise + * noop. + */ + fun maybeAddWifiConnectedListener() { + if (settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) == AUTOPLAY_ALLOW_ON_WIFI) { + addWifiConnectedListener() + } + } + + fun addWifiConnectedListener() { + wifiConnectionMonitor.addOnWifiConnectedChangedListener(wifiConnectedListener) + } + + fun removeWifiConnectedListener() { + wifiConnectionMonitor.removeOnWifiConnectedChangedListener(wifiConnectedListener) + } + + // Until https://bugzilla.mozilla.org/show_bug.cgi?id=1621825 is fixed, AUTOPLAY_ALLOW_ALL + // only works while WIFI is active, so we are not using AUTOPLAY_ALLOW_ON_WIFI (or this class). + // Once that is fixed, [start] and [maybeAddWifiConnectedListener] will need to be called on + // activity startup. + fun start() { wifiConnectionMonitor.start() } + + fun stop() { wifiConnectionMonitor.stop() } +} diff --git a/app/src/main/res/layout/fragment_manage_site_permissions_feature_phone.xml b/app/src/main/res/layout/fragment_manage_site_permissions_feature_phone.xml index e83c6b3ac..a4b9979ac 100644 --- a/app/src/main/res/layout/fragment_manage_site_permissions_feature_phone.xml +++ b/app/src/main/res/layout/fragment_manage_site_permissions_feature_phone.xml @@ -3,11 +3,11 @@ - 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/. --> + android:layout_height="fill_parent"> + android:paddingBottom="@dimen/radio_button_preference_vertical" + android:textAppearance="?android:attr/textAppearanceListItem" + app:drawableStartCompat="?android:attr/listChoiceIndicatorSingle" + tools:text="@string/preference_option_phone_feature_ask_to_allow" /> + + + + + android:paddingBottom="@dimen/radio_button_preference_vertical" + android:textAppearance="?android:attr/textAppearanceListItem" + app:drawableStartCompat="?android:attr/listChoiceIndicatorSingle" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e10385fb8..afbe9774d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -596,18 +596,16 @@ On Off - - Video and audio blocked - - Video and audio allowed Allow audio and video - - Allow audio and video on Wi-Fi only + + Block audio and video on cellular data only + + Audio and video will play on Wi-Fi - Block audio + Block audio only - Block video and audio + Block audio and video On diff --git a/app/src/main/res/xml/site_permissions_preferences.xml b/app/src/main/res/xml/site_permissions_preferences.xml index 0d4bd6fc6..d91fb3bd7 100644 --- a/app/src/main/res/xml/site_permissions_preferences.xml +++ b/app/src/main/res/xml/site_permissions_preferences.xml @@ -9,7 +9,7 @@ android:icon="@drawable/ic_autoplay_enabled" android:key="@string/pref_key_browser_feature_autoplay_audible" android:title="@string/preference_browser_feature_autoplay" - android:summary="@string/preference_option_autoplay_blocked"/> + android:summary="@string/preference_option_autoplay_blocked3"/>