Skip to content
This repository has been archived by the owner on Jun 17, 2024. It is now read-only.

Commit

Permalink
Bug 1861459 - Remove Home Screen usages of BrowsingModeManager
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthewTighe committed Feb 9, 2024
1 parent d5fa393 commit ea62d47
Show file tree
Hide file tree
Showing 17 changed files with 426 additions and 116 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import org.mozilla.fenix.helpers.HomeActivityTestRule
*
* Say no to main thread IO! 🙅
*/
private const val EXPECTED_SUPPRESSION_COUNT = 16
private const val EXPECTED_SUPPRESSION_COUNT = 15

/**
* The number of times we call the `runBlocking` coroutine method on the main thread during this
Expand Down
56 changes: 56 additions & 0 deletions fenix/app/src/main/java/org/mozilla/fenix/BrowsingModeBinding.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* 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

import android.view.Window
import android.view.WindowManager
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.withContext
import mozilla.components.lib.state.helpers.AbstractBinding
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.Settings

/**
* Binding to react to Private Browsing Mode changes in AppState.
*
* @param appStore AppStore to observe state changes from.
* @param themeManager Theme will be updated based on state changes.
* @param retrieveWindow Get window to update privacy flags for.
* @param settings Determine user settings for privacy features.
* @param ioDispatcher Dispatcher to launch disk reads. Exposed for test.
*/
class BrowsingModeBinding(
appStore: AppStore,
private val themeManager: ThemeManager,
private val retrieveWindow: () -> Window,
private val settings: Settings,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : AbstractBinding<AppState>(appStore) {
override suspend fun onState(flow: Flow<AppState>) {
flow.distinctUntilChangedBy { it.mode }.collect {
themeManager.currentTheme = it.mode
setWindowPrivacy(it.mode)
}
}

private suspend fun setWindowPrivacy(mode: BrowsingMode) {
if (mode == BrowsingMode.Private) {
val allowScreenshots = withContext(ioDispatcher) {
settings.allowScreenshotsInPrivateMode
}
if (!allowScreenshots) {
retrieveWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
} else {
retrieveWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* 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

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.utils.Settings

/**
* Middleware for controlling side-effects relating to Private Browsing Mode.
*
* @param settings Used to update disk-cache related PBM data.
* @param scope Scope used for disk writes and reads. Exposed for test overrides.
*/
class BrowsingModePersistenceMiddleware(
private val settings: Settings,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
) : Middleware<AppState, AppAction> {
override fun invoke(
context: MiddlewareContext<AppState, AppAction>,
next: (AppAction) -> Unit,
action: AppAction,
) {
val initialMode = context.state.mode
next(action)
val updatedMode = context.state.mode
if (initialMode != context.state.mode) {
scope.launch {
settings.lastKnownMode = updatedMode
}
}
when (action) {
is AppAction.Init -> {
scope.launch {
val mode = settings.lastKnownMode
context.store.dispatch(AppAction.BrowsingModeLoaded(mode))
}
}
else -> Unit
}
}
}
84 changes: 33 additions & 51 deletions fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.WindowManager.LayoutParams.FLAG_SECURE
import androidx.annotation.CallSuper
import androidx.annotation.IdRes
import androidx.annotation.RequiresApi
Expand Down Expand Up @@ -86,9 +85,10 @@ import org.mozilla.fenix.GleanMetrics.SplashScreen
import org.mozilla.fenix.GleanMetrics.StartOnHome
import org.mozilla.fenix.addons.ExtensionsProcessDisabledBackgroundController
import org.mozilla.fenix.addons.ExtensionsProcessDisabledForegroundController
import org.mozilla.fenix.bindings.BrowserStoreBinding
import org.mozilla.fenix.browser.browsingmode.AppStoreBrowsingModeManagerWrapper
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
import org.mozilla.fenix.components.metrics.GrowthDataWorker
Expand Down Expand Up @@ -152,8 +152,12 @@ import java.util.Locale
@SuppressWarnings("TooManyFunctions", "LargeClass", "LongMethod")
open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
private lateinit var binding: ActivityHomeBinding
lateinit var themeManager: ThemeManager
lateinit var browsingModeManager: BrowsingModeManager
val browsingModeManager: BrowsingModeManager by lazy {
AppStoreBrowsingModeManagerWrapper(components.appStore)
}
val themeManager by lazy {
createThemeManager()
}

private var isVisuallyComplete = false

Expand Down Expand Up @@ -231,14 +235,13 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {

maybeShowSplashScreen()

// There is disk read violations on some devices such as samsung and pixel for android 9/10
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
// Browsing mode & theme setup should always be called before super.onCreate.
setupBrowsingMode(getModeFromIntentOrLastKnown(intent))
setupTheme()

super.onCreate(savedInstanceState)
setModeFromIntent(intent)
// Theme setup should always be called before super.onCreate
if (this !is ExternalAppBrowserActivity) {
themeManager.setActivityTheme(this)
themeManager.applyStatusBarTheme(this)
}
super.onCreate(savedInstanceState)

// Checks if Activity is currently in PiP mode if launched from external intents, then exits it
checkAndExitPiP()
Expand Down Expand Up @@ -369,6 +372,13 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
extensionsProcessDisabledBackgroundController,
serviceWorkerSupport,
webExtensionPromptFeature,
BrowserStoreBinding(components.core.store, components.appStore),
BrowsingModeBinding(
components.appStore,
themeManager,
{ window },
components.settings,
),
)

if (shouldAddToRecentsScreen(intent)) {
Expand Down Expand Up @@ -677,7 +687,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
) + externalSourceIntentProcessors
val intentHandled =
intentProcessors.any { it.process(intent, navHost.navController, this.intent) }
browsingModeManager.mode = getModeFromIntentOrLastKnown(intent)
setModeFromIntent(intent)

if (intentHandled) {
supportFragmentManager
Expand Down Expand Up @@ -848,15 +858,18 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
* private mode directly before the content view is created. Returns the mode set by the intent
* otherwise falls back to the last known mode.
*/
@VisibleForTesting
internal fun getModeFromIntentOrLastKnown(intent: Intent?): BrowsingMode {
internal fun setModeFromIntent(intent: Intent?) {
intent?.toSafeIntent()?.let {
if (it.hasExtra(PRIVATE_BROWSING_MODE)) {
val startPrivateMode = it.getBooleanExtra(PRIVATE_BROWSING_MODE, false)
return BrowsingMode.fromBoolean(isPrivate = startPrivateMode)
// Since the mode is initially set from settings during AppAction.Init, this action
// will overwrite the state once the action is processed in the queue if called from
// onCreate.
if (startPrivateMode) {
components.appStore.dispatch(AppAction.IntentAction.EnterPrivateBrowsing)
}
}
}
return settings().lastKnownMode
}

/**
Expand All @@ -872,20 +885,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
return false
}

private fun setupBrowsingMode(mode: BrowsingMode) {
settings().lastKnownMode = mode
browsingModeManager = createBrowsingModeManager(mode)
}

private fun setupTheme() {
themeManager = createThemeManager()
// ExternalAppBrowserActivity handles it's own theming as it can be customized.
if (this !is ExternalAppBrowserActivity) {
themeManager.setActivityTheme(this)
themeManager.applyStatusBarTheme(this)
}
}

// Stop active media when activity is destroyed.
private fun stopMediaSession() {
if (isFinishing) {
Expand Down Expand Up @@ -1009,7 +1008,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
additionalHeaders: Map<String, String>? = null,
) {
val startTime = components.core.engine.profiler?.getProfilerTime()
val mode = browsingModeManager.mode
val mode = components.appStore.state.mode

val private = when (mode) {
BrowsingMode.Private -> true
Expand Down Expand Up @@ -1090,7 +1089,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {

// Normal tabs + cold start -> Should go back to browser if we had any tabs open when we left last
// except for PBM + Cold Start there won't be any tabs since they're evicted so we never will navigate
if (settings().shouldReturnToBrowser && !browsingModeManager.mode.isPrivate) {
if (settings().shouldReturnToBrowser && !components.appStore.state.mode.isPrivate) {
// Navigate to home first (without rendering it) to add it to the back stack.
openToBrowser(BrowserDirection.FromGlobal, null)
}
Expand Down Expand Up @@ -1125,25 +1124,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
return super.getSystemService(name)
}

private fun createBrowsingModeManager(initialMode: BrowsingMode): BrowsingModeManager {
return DefaultBrowsingModeManager(initialMode, components.settings) { newMode ->
updateSecureWindowFlags(newMode)
themeManager.currentTheme = newMode
}.also {
updateSecureWindowFlags(initialMode)
}
}

private fun updateSecureWindowFlags(mode: BrowsingMode = browsingModeManager.mode) {
if (mode == BrowsingMode.Private && !settings().allowScreenshotsInPrivateMode) {
window.addFlags(FLAG_SECURE)
} else {
window.clearFlags(FLAG_SECURE)
}
}

private fun createThemeManager(): ThemeManager {
return DefaultThemeManager(browsingModeManager.mode, this)
protected open fun createThemeManager(): ThemeManager {
return DefaultThemeManager(components.appStore.state.mode, this)
}

private fun openPopup(webExtensionState: WebExtensionState) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

package org.mozilla.fenix.browser.browsingmode

import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.utils.Settings

/**
Expand All @@ -17,6 +19,11 @@ enum class BrowsingMode {
*/
val isPrivate get() = this == Private

val inverted get() = when (this) {
Private -> Normal
Normal -> Private
}

companion object {

/**
Expand All @@ -31,6 +38,19 @@ interface BrowsingModeManager {
var mode: BrowsingMode
}

/**
* Wraps an [appStore] and keeps its [AppState.mode] in sync with external changes.
*/
class AppStoreBrowsingModeManagerWrapper(
private val appStore: AppStore,
) : BrowsingModeManager {
override var mode: BrowsingMode
get() = appStore.state.mode
set(value) {
appStore.dispatch(AppAction.ModeChange(value))
}
}

/**
* Wraps a [BrowsingMode] and executes a callback whenever [mode] is updated.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ import org.mozilla.fenix.components.appstate.AppStoreReducer
class AppStore(
initialState: AppState = AppState(),
middlewares: List<Middleware<AppState, AppAction>> = emptyList(),
) : Store<AppState, AppAction>(initialState, AppStoreReducer::reduce, middlewares)
) : Store<AppState, AppAction>(initialState, AppStoreReducer::reduce, middlewares) {
init {
dispatch(AppAction.Init)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import mozilla.components.feature.downloads.manager.FetchDownloadManager
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.base.android.NotificationsDelegate
import mozilla.components.support.base.worker.Frequency
import org.mozilla.fenix.BrowsingModePersistenceMiddleware
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
Expand Down Expand Up @@ -234,6 +235,9 @@ class Components(private val context: Context) {
messagingStorage = analytics.messagingStorage,
),
MetricsMiddleware(metrics = analytics.metrics),
BrowsingModePersistenceMiddleware(
settings = settings,
),
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ import org.mozilla.fenix.wallpapers.Wallpaper
* [Action] implementation related to [AppStore].
*/
sealed class AppAction : Action {
/**
* [AppAction] dispatched to indicate that the store is initialized and
* ready to use. This action is dispatched automatically before any other
* action is processed. Its main purpose is to trigger initialization logic
* in middlewares. The action itself should have no effect on the [AppState].
*/
object Init : AppAction()

/**
* The browsing [mode] has been loaded from a persistence layer.
*/
data class BrowsingModeLoaded(val mode: BrowsingMode) : AppAction()

data class UpdateInactiveExpanded(val expanded: Boolean) : AppAction()

/**
Expand Down Expand Up @@ -267,4 +280,14 @@ sealed class AppAction : Action {
val key: ShoppingState.ProductRecommendationImpressionKey,
) : ShoppingAction()
}

/**
* Actions related to Intents.
*/
sealed class IntentAction : AppAction() {
/**
* Private browsing mode should be entered.
*/
object EnterPrivateBrowsing : IntentAction()
}
}
Loading

0 comments on commit ea62d47

Please sign in to comment.