Skip to content

Commit

Permalink
For mozilla-mobile#26511: Parallelize work for setting the wallpaper
Browse files Browse the repository at this point in the history
Split loading the bitmap from storage and actually setting it in two operations
with one that can run in parallel with onCreateView for HomeFragment and one
that can be used serially on the main thread to actually set the wallpaper.

This seems like the best compromise to ensure that everytime the homescreen is
shown it will have the wallpaper set but does affect the performance - there is
a delay in showing HomeFragment to account for waiting for the wallpaper to be
set.
In testing the new delay seems close to the one from the initial wallpapers
implementation. See more in mozilla-mobile#26794.
  • Loading branch information
Mugurell committed Sep 12, 2022
1 parent 27e2402 commit 00e34f6
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 115 deletions.
8 changes: 7 additions & 1 deletion app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ class HomeFragment : Fragment() {
if (shouldEnableWallpaper()) {
wallpapersObserver = WallpapersObserver(
appStore = components.appStore,
settings = requireContext().settings(),
wallpapersUseCases = components.useCases.wallpaperUseCases,
wallpaperImageView = binding.wallpaperImageView,
).also {
Expand Down Expand Up @@ -418,7 +419,6 @@ class HomeFragment : Fragment() {
getMenuButton()?.dismissMenu()

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,
Expand Down Expand Up @@ -765,6 +765,12 @@ class HomeFragment : Fragment() {
lifecycleScope.launch(IO) {
requireComponents.reviewPromptController.promptReview(requireActivity())
}

if (shouldEnableWallpaper()) {
runBlockingIncrement {
wallpapersObserver?.applyCurrentWallpaper()
}
}
}

private fun dispatchModeChanges(mode: Mode) {
Expand Down
90 changes: 69 additions & 21 deletions app/src/main/java/org/mozilla/fenix/home/WallpapersObserver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,27 @@

package org.mozilla.fenix.home

import android.graphics.Bitmap
import android.widget.ImageView
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import androidx.core.view.isVisible
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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.utils.Settings
import org.mozilla.fenix.wallpapers.Wallpaper
import org.mozilla.fenix.wallpapers.WallpapersUseCases

Expand All @@ -28,19 +34,44 @@ import org.mozilla.fenix.wallpapers.WallpapersUseCases
* when the [LifecycleOwner] is destroyed.
*
* @param appStore Holds the details abut the current wallpaper.
* @param settings Used for checking user's option for what wallpaper to use.
* @param wallpapersUseCases Used for interacting with the wallpaper feature.
* @param wallpaperImageView Serves as the target when applying wallpapers.
* @param backgroundWorkDispatcher Used for scheduling the wallpaper update when the state is updated
* with a new wallpaper.
*/
class WallpapersObserver(
private val appStore: AppStore,
private val settings: Settings,
private val wallpapersUseCases: WallpapersUseCases,
private val wallpaperImageView: ImageView,
backgroundWorkDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : DefaultLifecycleObserver {
@VisibleForTesting
internal var observeWallpapersStoreSubscription: Store.Subscription<AppState, AppAction>? = null

/**
* Coroutine scope for updating the wallpapers when an update is observed.
* Allows for easy cleanup when the client of this is destroyed.
*/
@VisibleForTesting
internal var wallpapersScope = CoroutineScope(backgroundWorkDispatcher)

/**
* Setting the wallpaper assumes two steps:
* - load - running on IO
* - set - running on Main
* This property caches the result of [loadWallpaper] to be later used by [applyCurrentWallpaper].
*/
@Volatile
@VisibleForTesting
internal var wallpapersScope = CoroutineScope(Dispatchers.Main.immediate)
internal var currentWallpaperImage: Bitmap? = null

/**
* Listener for when the first observed wallpaper is loaded and available to be set.
*/
@VisibleForTesting
internal val isWallpaperLoaded = CompletableDeferred<Unit>()

init {
observeWallpaperUpdates()
Expand All @@ -50,8 +81,20 @@ class WallpapersObserver(
* Immediately apply the current wallpaper automatically adjusted to support
* the current configuration - portrait or landscape.
*/
fun applyCurrentWallpaper() {
showWallpaper()
internal suspend fun applyCurrentWallpaper() {
isWallpaperLoaded.await()

withContext(Dispatchers.Main.immediate) {
with(currentWallpaperImage) {
when (this) {
null -> wallpaperImageView.isVisible = false
else -> {
scaleToBottomOfView(wallpaperImageView)
wallpaperImageView.isVisible = true
}
}
}
}
}

override fun onDestroy(owner: LifecycleOwner) {
Expand All @@ -64,37 +107,42 @@ class WallpapersObserver(
var lastObservedValue: Wallpaper? = null
observeWallpapersStoreSubscription = appStore.observeManually { state ->
val currentValue = state.wallpaperState.currentWallpaper

// Use the persisted wallpaper name to wait until a state update
// that contains the wallpaper that the user chose.
// Avoids setting the AppState default wallpaper if we know that another wallpaper is chosen.
if (currentValue.name != settings.currentWallpaperName) {
return@observeManually
}

// 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.
// Avoids setting the same wallpaper twice.
if (currentValue.name != lastObservedValue?.name) {
lastObservedValue = currentValue

showWallpaper(currentValue)
wallpapersScope.launch {
loadWallpaper(currentValue)
applyCurrentWallpaper()
}
}
}.also {
it.resume()
}
}

/**
* Load the bitmap of [wallpaper] and cache it in [currentWallpaperImage].
*/
@WorkerThread
@VisibleForTesting
internal fun showWallpaper(wallpaper: Wallpaper = appStore.state.wallpaperState.currentWallpaper) {
wallpapersScope.launch {
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
}
}
}
internal suspend fun loadWallpaper(wallpaper: Wallpaper) {
currentWallpaperImage = when (wallpaper) {
Wallpaper.Default -> null
else -> wallpapersUseCases.loadBitmap(wallpaper)
}

isWallpaperLoaded.complete(Unit)
}
}
Loading

0 comments on commit 00e34f6

Please sign in to comment.