For #26555 - Observe and update the wallpaper before HomeScreen is visible.
By using Store.observeManually in a standalone coroutine we can observe the store and update the wallpapers even before onStart (in manual tests is right around onStart, certainly before the other widgets shown on homescreen). Created a new WallpapersObserver to have the functionality easier to reason about and be easier to test.
This commit is contained in:
parent
d314c1102b
commit
ab3f6b5e4b
|
@ -45,9 +45,7 @@ import kotlinx.coroutines.Dispatchers.IO
|
|||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.browser.menu.view.MenuButton
|
||||
import mozilla.components.browser.state.selector.findTab
|
||||
|
@ -67,7 +65,6 @@ import mozilla.components.feature.top.sites.TopSitesFrecencyConfig
|
|||
import mozilla.components.feature.top.sites.TopSitesProviderConfig
|
||||
import mozilla.components.lib.state.ext.consumeFlow
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import mozilla.components.lib.state.ext.flow
|
||||
import mozilla.components.service.glean.private.NoExtras
|
||||
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
||||
import mozilla.components.support.ktx.android.content.res.resolveAttribute
|
||||
|
@ -91,7 +88,6 @@ import org.mozilla.fenix.ext.hideToolbar
|
|||
import org.mozilla.fenix.ext.nav
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.runIfFragmentIsAttached
|
||||
import org.mozilla.fenix.ext.scaleToBottomOfView
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.gleanplumb.DefaultMessageController
|
||||
import org.mozilla.fenix.gleanplumb.MessagingFeature
|
||||
|
@ -119,7 +115,6 @@ import org.mozilla.fenix.tabstray.TabsTrayAccessPoint
|
|||
import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD
|
||||
import org.mozilla.fenix.utils.ToolbarPopupWindow
|
||||
import org.mozilla.fenix.utils.allowUndo
|
||||
import org.mozilla.fenix.wallpapers.Wallpaper
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.math.min
|
||||
|
||||
|
@ -167,6 +162,8 @@ class HomeFragment : Fragment() {
|
|||
private var sessionControlView: SessionControlView? = null
|
||||
private var appBarLayout: AppBarLayout? = null
|
||||
private lateinit var currentMode: CurrentMode
|
||||
@VisibleForTesting
|
||||
internal lateinit var wallpapersObserver: WallpapersObserver
|
||||
|
||||
private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>()
|
||||
private val messagingFeature = ViewBoundFeatureWrapper<MessagingFeature>()
|
||||
|
@ -212,9 +209,15 @@ class HomeFragment : Fragment() {
|
|||
val activity = activity as HomeActivity
|
||||
val components = requireComponents
|
||||
|
||||
// See https://github.com/mozilla-mobile/fenix/issues/26555 for context as to why
|
||||
// this is commented out for now
|
||||
observeWallpaperChanges()
|
||||
if (shouldEnableWallpaper()) {
|
||||
wallpapersObserver = WallpapersObserver(
|
||||
appStore = components.appStore,
|
||||
wallpapersUseCases = components.useCases.wallpaperUseCases,
|
||||
wallpaperImageView = binding.wallpaperImageView,
|
||||
).also {
|
||||
viewLifecycleOwner.lifecycle.addObserver(it)
|
||||
}
|
||||
}
|
||||
|
||||
currentMode = CurrentMode(
|
||||
requireContext(),
|
||||
|
@ -410,7 +413,17 @@ class HomeFragment : Fragment() {
|
|||
super.onConfigurationChanged(newConfig)
|
||||
|
||||
getMenuButton()?.dismissMenu()
|
||||
setWallpaperToCurrent()
|
||||
|
||||
if (shouldEnableWallpaper()) {
|
||||
// Setting the wallpaper is a potentially expensive operation - can take 100ms.
|
||||
// Running this on the Main thread helps to ensure that the just updated configuration
|
||||
// will be used when the wallpaper is scaled to match.
|
||||
// Otherwise the portrait wallpaper may remain shown on landscape,
|
||||
// see https://github.com/mozilla-mobile/fenix/issues/26638
|
||||
runBlockingIncrement {
|
||||
wallpapersObserver.applyCurrentWallpaper()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -927,43 +940,8 @@ class HomeFragment : Fragment() {
|
|||
?.isVisible = tabCount > 0
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun observeWallpaperChanges() {
|
||||
if (shouldEnableWallpaper()) {
|
||||
requireComponents.appStore.flow()
|
||||
.ifChanged { state -> state.wallpaperState.currentWallpaper }
|
||||
.onEach { state ->
|
||||
showWallpaper(state.wallpaperState.currentWallpaper)
|
||||
}
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setWallpaperToCurrent() {
|
||||
if (shouldEnableWallpaper()) {
|
||||
val wallpaper = requireComponents.appStore.state.wallpaperState.currentWallpaper
|
||||
runBlockingIncrement {
|
||||
showWallpaper(wallpaper)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun showWallpaper(wallpaper: Wallpaper) = when (wallpaper) {
|
||||
// We only want to update the wallpaper when it's different from the default one
|
||||
// as the default is applied already on xml by default.
|
||||
Wallpaper.Default -> {
|
||||
binding.wallpaperImageView.visibility = View.GONE
|
||||
}
|
||||
else -> {
|
||||
val bitmap = requireComponents.useCases.wallpaperUseCases.loadBitmap(wallpaper)
|
||||
bitmap?.let {
|
||||
it.scaleToBottomOfView(binding.wallpaperImageView)
|
||||
binding.wallpaperImageView.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldEnableWallpaper() =
|
||||
@VisibleForTesting
|
||||
internal fun shouldEnableWallpaper() =
|
||||
(activity as? HomeActivity)?.themeManager?.currentTheme?.isPrivate?.not() ?: false
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
/* 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.home
|
||||
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.lib.state.Store
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
import org.mozilla.fenix.components.appstate.AppAction
|
||||
import org.mozilla.fenix.components.appstate.AppState
|
||||
import org.mozilla.fenix.ext.scaleToBottomOfView
|
||||
import org.mozilla.fenix.wallpapers.Wallpaper
|
||||
import org.mozilla.fenix.wallpapers.WallpapersUseCases
|
||||
|
||||
/**
|
||||
* [LifecycleObserver] that will immediately start observing the store for wallpapers updates
|
||||
* to apply them to the passed in [wallpaperImageView] and automatically stop observing for updates
|
||||
* when the [LifecycleOwner] is destroyed.
|
||||
*
|
||||
* @param appStore Holds the details abut the current wallpaper.
|
||||
* @param wallpapersUseCases Used for interacting with the wallpaper feature.
|
||||
* @param wallpaperImageView Serves as the target when applying wallpapers.
|
||||
*/
|
||||
class WallpapersObserver(
|
||||
private val appStore: AppStore,
|
||||
private val wallpapersUseCases: WallpapersUseCases,
|
||||
private val wallpaperImageView: ImageView,
|
||||
) : DefaultLifecycleObserver {
|
||||
@VisibleForTesting
|
||||
internal var observeWallpapersStoreSubscription: Store.Subscription<AppState, AppAction>? = null
|
||||
@VisibleForTesting
|
||||
internal var wallpapersScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
init {
|
||||
observeWallpaperUpdates()
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately apply the current wallpaper automatically adjusted to support
|
||||
* the current configuration - portrait or landscape.
|
||||
*/
|
||||
suspend fun applyCurrentWallpaper() {
|
||||
showWallpaper()
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
observeWallpapersStoreSubscription?.unsubscribe()
|
||||
wallpapersScope.cancel()
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun observeWallpaperUpdates() {
|
||||
var lastObservedValue: Wallpaper? = null
|
||||
observeWallpapersStoreSubscription = appStore.observeManually { state ->
|
||||
val currentValue = state.wallpaperState.currentWallpaper
|
||||
// Use the wallpaper name to differentiate between updates to properly support
|
||||
// the restored from settings wallpaper being the same as the one downloaded
|
||||
// case in which details like "collection" may be different.
|
||||
if (currentValue.name != lastObservedValue?.name) {
|
||||
lastObservedValue = currentValue
|
||||
|
||||
wallpapersScope.launch { showWallpaper(currentValue) }
|
||||
}
|
||||
}.also {
|
||||
it.resume()
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal suspend fun showWallpaper(wallpaper: Wallpaper = appStore.state.wallpaperState.currentWallpaper) {
|
||||
when (wallpaper) {
|
||||
// We only want to update the wallpaper when it's different from the default one
|
||||
// as the default is applied already on xml by default.
|
||||
Wallpaper.Default -> {
|
||||
wallpaperImageView.isVisible = false
|
||||
}
|
||||
else -> {
|
||||
val bitmap = wallpapersUseCases.loadBitmap(wallpaper)
|
||||
bitmap?.let {
|
||||
it.scaleToBottomOfView(wallpaperImageView)
|
||||
wallpaperImageView.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,11 +5,13 @@
|
|||
package org.mozilla.fenix.home
|
||||
|
||||
import android.content.Context
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.spyk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import mozilla.components.browser.menu.view.MenuButton
|
||||
import mozilla.components.browser.state.state.SearchState
|
||||
import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
|
||||
|
@ -21,6 +23,7 @@ import org.junit.Assert.assertTrue
|
|||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.FenixApplication
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.components.Core
|
||||
import org.mozilla.fenix.ext.application
|
||||
import org.mozilla.fenix.ext.components
|
||||
|
@ -103,4 +106,54 @@ class HomeFragmentTest {
|
|||
|
||||
verify(exactly = 1) { menuButton.dismissMenu() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the user is in normal mode WHEN configuration changes THEN the wallpaper is reapplied`() = runTest {
|
||||
homeFragment.getMenuButton = { null }
|
||||
val observer: WallpapersObserver = mockk(relaxed = true)
|
||||
homeFragment.wallpapersObserver = observer
|
||||
val activity: HomeActivity = mockk {
|
||||
every { themeManager.currentTheme.isPrivate } returns false
|
||||
}
|
||||
every { homeFragment.activity } returns activity
|
||||
|
||||
homeFragment.onConfigurationChanged(mockk(relaxed = true))
|
||||
|
||||
coVerify { observer.applyCurrentWallpaper() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the user is in private mode WHEN configuration changes THEN the wallpaper not updated`() = runTest {
|
||||
homeFragment.getMenuButton = { null }
|
||||
val observer: WallpapersObserver = mockk(relaxed = true)
|
||||
homeFragment.wallpapersObserver = observer
|
||||
val activity: HomeActivity = mockk {
|
||||
every { themeManager.currentTheme.isPrivate } returns true
|
||||
}
|
||||
every { homeFragment.activity } returns activity
|
||||
|
||||
homeFragment.onConfigurationChanged(mockk(relaxed = true))
|
||||
|
||||
coVerify(exactly = 0) { observer.applyCurrentWallpaper() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the user is in normal mode WHEN checking if should enable wallpaper THEN return true`() {
|
||||
val activity: HomeActivity = mockk {
|
||||
every { themeManager.currentTheme.isPrivate } returns false
|
||||
}
|
||||
every { homeFragment.activity } returns activity
|
||||
|
||||
assertTrue(homeFragment.shouldEnableWallpaper())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the user is in private mode WHEN checking if should enable wallpaper THEN return false`() {
|
||||
val activity: HomeActivity = mockk {
|
||||
every { themeManager.currentTheme.isPrivate } returns true
|
||||
}
|
||||
every { homeFragment.activity } returns activity
|
||||
|
||||
assertFalse(homeFragment.shouldEnableWallpaper())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
/* 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.home
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.widget.ImageView
|
||||
import androidx.core.view.isVisible
|
||||
import io.mockk.Called
|
||||
import io.mockk.Runs
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.spyk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.cancel
|
||||
import mozilla.components.support.test.libstate.ext.waitUntilIdle
|
||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||
import mozilla.components.support.test.rule.runTestOnMain
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
import org.mozilla.fenix.components.appstate.AppAction.WallpaperAction.UpdateCurrentWallpaper
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.wallpapers.Wallpaper
|
||||
import org.mozilla.fenix.wallpapers.WallpapersUseCases
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class WallpapersObserverTest {
|
||||
@get:Rule
|
||||
val coroutinesTestRule = MainCoroutineRule()
|
||||
|
||||
@Test
|
||||
fun `WHEN the observer is created THEN start observing the store`() {
|
||||
val appStore: AppStore = mockk(relaxed = true) {
|
||||
every { observeManually(any()) } answers { mockk(relaxed = true) }
|
||||
}
|
||||
|
||||
val observer = WallpapersObserver(appStore, mockk(), mockk())
|
||||
|
||||
assertNotNull(observer.observeWallpapersStoreSubscription)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN asked to apply the wallpaper THEN show it`() = runTestOnMain {
|
||||
val appStore = AppStore()
|
||||
val observer = spyk(WallpapersObserver(appStore, mockk(), mockk())) {
|
||||
coEvery { showWallpaper(any()) } just Runs
|
||||
}
|
||||
|
||||
observer.applyCurrentWallpaper()
|
||||
|
||||
coVerify { observer.showWallpaper(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the store was observed for updates WHEN the lifecycle owner is destroyed THEN stop observing the store`() {
|
||||
val observer = WallpapersObserver(mockk(relaxed = true), mockk(), mockk())
|
||||
observer.observeWallpapersStoreSubscription = mockk(relaxed = true)
|
||||
observer.wallpapersScope = mockk {
|
||||
every { cancel() } just Runs
|
||||
}
|
||||
|
||||
observer.onDestroy(mockk())
|
||||
|
||||
verify { observer.wallpapersScope.cancel() }
|
||||
verify { observer.observeWallpapersStoreSubscription!!.unsubscribe() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the wallpaper is updated THEN show the wallpaper`() = runTestOnMain {
|
||||
val appStore = AppStore()
|
||||
val observer = spyk(WallpapersObserver(appStore, mockk(relaxed = true), mockk(relaxed = true))) {
|
||||
coEvery { showWallpaper(any()) } just Runs
|
||||
}
|
||||
|
||||
// Ignore the call on the real instance and call again "observeWallpaperUpdates"
|
||||
// on the spy to be able to verify the "showWallpaper" call in the spy.
|
||||
observer.observeWallpaperUpdates()
|
||||
|
||||
val newWallpaper: Wallpaper = mockk(relaxed = true)
|
||||
appStore.dispatch(UpdateCurrentWallpaper(newWallpaper))
|
||||
appStore.waitUntilIdle()
|
||||
coVerify { observer.showWallpaper(newWallpaper) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the wallpaper is updated to a new one THEN show the wallpaper`() = runTestOnMain {
|
||||
val appStore = AppStore()
|
||||
val wallpapersUseCases: WallpapersUseCases = mockk {
|
||||
coEvery { loadBitmap(any()) } returns null
|
||||
}
|
||||
val observer = spyk(WallpapersObserver(appStore, wallpapersUseCases, mockk(relaxed = true))) {
|
||||
coEvery { showWallpaper(any()) } just Runs
|
||||
}
|
||||
|
||||
// Ignore the call on the real instance and call again "observeWallpaperUpdates"
|
||||
// on the spy to be able to verify the "showWallpaper" call in the spy.
|
||||
observer.observeWallpaperUpdates()
|
||||
coVerify { observer.showWallpaper(Wallpaper.Default) }
|
||||
|
||||
val wallpaper: Wallpaper = mockk(relaxed = true)
|
||||
appStore.dispatch(UpdateCurrentWallpaper(wallpaper))
|
||||
appStore.waitUntilIdle()
|
||||
coVerify { observer.showWallpaper(wallpaper) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the wallpaper is updated to the current one THEN don't try showing the same wallpaper again`() = runTestOnMain {
|
||||
val appStore = AppStore()
|
||||
val wallpapersUseCases: WallpapersUseCases = mockk {
|
||||
coEvery { loadBitmap(any()) } returns null
|
||||
}
|
||||
val observer = spyk(WallpapersObserver(appStore, wallpapersUseCases, mockk(relaxed = true))) {
|
||||
coEvery { showWallpaper(any()) } just Runs
|
||||
}
|
||||
// Ignore the call on the real instance and call again "observeWallpaperUpdates"
|
||||
// on the spy to be able to verify the "showWallpaper" call in the spy.
|
||||
observer.observeWallpaperUpdates()
|
||||
|
||||
val wallpaper: Wallpaper = mockk(relaxed = true)
|
||||
appStore.dispatch(UpdateCurrentWallpaper(wallpaper))
|
||||
appStore.waitUntilIdle()
|
||||
coVerify { observer.showWallpaper(wallpaper) }
|
||||
|
||||
appStore.dispatch(UpdateCurrentWallpaper(wallpaper))
|
||||
appStore.waitUntilIdle()
|
||||
coVerify(exactly = 1) { observer.showWallpaper(wallpaper) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no wallpaper is provided WHEN asked to show the wallpaper THEN show the current one`() = runTestOnMain {
|
||||
val wallpaper: Wallpaper = mockk()
|
||||
val appStore: AppStore = mockk(relaxed = true) {
|
||||
every { state.wallpaperState.currentWallpaper } returns wallpaper
|
||||
}
|
||||
val observer = spyk(WallpapersObserver(appStore, mockk(relaxed = true), mockk(relaxed = true)))
|
||||
|
||||
observer.showWallpaper()
|
||||
|
||||
coVerify { observer.showWallpaper(wallpaper) }
|
||||
}
|
||||
|
||||
fun `GiVEN the current wallpaper is the default one WHEN showing it THEN hide the wallpaper view`() = runTestOnMain {
|
||||
val wallpapersUseCases: WallpapersUseCases = mockk()
|
||||
val wallpaperView: ImageView = mockk(relaxed = true)
|
||||
val observer = WallpapersObserver(mockk(relaxed = true), wallpapersUseCases, wallpaperView)
|
||||
|
||||
observer.showWallpaper(Wallpaper.Default)
|
||||
|
||||
verify { wallpaperView.isVisible = false }
|
||||
verify { wallpapersUseCases wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GiVEN the current wallpaper is different than the default one WHEN showing it THEN load it's bitmap in the visible wallpaper view`() = runTestOnMain {
|
||||
val wallpaper: Wallpaper = mockk()
|
||||
val bitmap: Bitmap = mockk()
|
||||
val wallpapersUseCases: WallpapersUseCases = mockk {
|
||||
coEvery { loadBitmap(any()) } returns bitmap
|
||||
}
|
||||
val wallpaperView: ImageView = mockk(relaxed = true)
|
||||
val observer = WallpapersObserver(mockk(relaxed = true), wallpapersUseCases, wallpaperView)
|
||||
|
||||
observer.showWallpaper(wallpaper)
|
||||
|
||||
verify { wallpaperView.isVisible = true }
|
||||
verify { wallpaperView.setImageBitmap(bitmap) }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue