blob: 46b2c1a9a2650e999efb6b9110f7de82e36f7174 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.android.wallpaper.picker.customization.ui.binder
import android.app.Activity
import android.app.WallpaperColors
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.service.wallpaper.WallpaperService
import android.view.SurfaceView
import android.view.View
import android.view.View.OnAttachStateChangeListener
import android.view.ViewGroup
import android.widget.ImageView
import androidx.cardview.widget.CardView
import androidx.core.view.isVisible
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.monet.ColorScheme
import com.android.wallpaper.R
import com.android.wallpaper.asset.Asset
import com.android.wallpaper.asset.BitmapCachingAsset
import com.android.wallpaper.asset.CurrentWallpaperAsset
import com.android.wallpaper.config.BaseFlags
import com.android.wallpaper.model.LiveWallpaperInfo
import com.android.wallpaper.model.WallpaperInfo
import com.android.wallpaper.module.CustomizationSections
import com.android.wallpaper.picker.FixedWidthDisplayRatioFrameLayout
import com.android.wallpaper.picker.WorkspaceSurfaceHolderCallback
import com.android.wallpaper.picker.customization.animation.view.LoadingAnimation
import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewClickView
import com.android.wallpaper.picker.customization.ui.view.WallpaperSurfaceView
import com.android.wallpaper.picker.customization.ui.viewmodel.AnimationStateViewModel
import com.android.wallpaper.picker.customization.ui.viewmodel.ScreenPreviewViewModel
import com.android.wallpaper.util.ResourceUtils
import com.android.wallpaper.util.WallpaperConnection
import com.android.wallpaper.util.WallpaperSurfaceCallback
import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.launch
/**
* Binds between view and view-model for rendering the preview of the home screen or the lock
* screen.
*/
object ScreenPreviewBinder {
interface Binding {
fun sendMessage(
id: Int,
args: Bundle = Bundle.EMPTY,
)
fun destroy()
fun surface(): SurfaceView
}
/**
* Binds the view to the given [viewModel].
*
* Note that if [dimWallpaper] is `true`, the wallpaper will be dimmed (to help highlight
* something that is changing on top of the wallpaper, for example, the lock screen shortcuts or
* the clock).
*/
// TODO (b/274443705): incorporate color picker to allow preview loading on color change
// TODO (b/274443705): make loading animation more continuous on reveal
// TODO (b/274443705): adjust for better timing on animation reveal
@JvmStatic
fun bind(
activity: Activity,
previewView: CardView,
viewModel: ScreenPreviewViewModel,
lifecycleOwner: LifecycleOwner,
offsetToStart: Boolean,
dimWallpaper: Boolean = false,
onWallpaperPreviewDirty: () -> Unit,
animationStateViewModel: AnimationStateViewModel? = null,
isWallpaperAlwaysVisible: Boolean = true,
mirrorSurface: SurfaceView? = null,
): Binding {
val workspaceSurface: SurfaceView = previewView.requireViewById(R.id.workspace_surface)
val wallpaperSurface: WallpaperSurfaceView =
previewView.requireViewById(R.id.wallpaper_surface)
val thumbnailRequested = AtomicBoolean(false)
// Tracks whether the live preview should be shown, since a) visibility updates may arrive
// before the engine is ready, and b) we need this state for onResume
// TODO(b/287618705) Remove this
val showLivePreview = AtomicBoolean(isWallpaperAlwaysVisible)
val fixedWidthDisplayFrameLayout = previewView.parent as? FixedWidthDisplayRatioFrameLayout
val screenPreviewClickView = fixedWidthDisplayFrameLayout?.parent as? ScreenPreviewClickView
if (screenPreviewClickView != null) {
// If screenPreviewClickView exists, we will have it handle accessibility and
// disable a11y for the descendants.
// Set the content description on the parent view
screenPreviewClickView.contentDescription =
activity.resources.getString(viewModel.previewContentDescription)
fixedWidthDisplayFrameLayout.importantForAccessibility =
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
// This ensures that we do not announce the time multiple times
previewView.importantForAccessibility =
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
}
var wallpaperIsReadyForReveal = false
val surfaceViewsReady = {
wallpaperSurface.setBackgroundColor(Color.TRANSPARENT)
workspaceSurface.visibility = View.VISIBLE
}
wallpaperSurface.setZOrderOnTop(false)
val flags = BaseFlags.get()
val isPageTransitionsFeatureEnabled = flags.isPageTransitionsFeatureEnabled(activity)
val showLoadingAnimation =
flags.isPreviewLoadingAnimationEnabled(activity.applicationContext)
var loadingAnimation: LoadingAnimation? = null
val loadingView: ImageView = previewView.requireViewById(R.id.loading_view)
if (dimWallpaper) {
previewView.requireViewById<View>(R.id.wallpaper_dimming_scrim).isVisible = true
workspaceSurface.setZOrderOnTop(true)
}
previewView.radius =
previewView.resources.getDimension(R.dimen.wallpaper_picker_entry_card_corner_radius)
previewView.isClickable = true
var previewSurfaceCallback: WorkspaceSurfaceHolderCallback? = null
var wallpaperSurfaceCallback: WallpaperSurfaceCallback? = null
var wallpaperConnection: WallpaperConnection? = null
var wallpaperInfo: WallpaperInfo? = null
var animationState: AnimationStateViewModel.AnimationState? = null
var loadingImageDrawable: Drawable? = null
var animationTimeToRestore: Long? = null
var animationTransitionProgress: Float? = null
var animationColorToRestore: Int? = null
var currentWallpaperThumbnail: Bitmap? = null
var disposableHandle: DisposableHandle? = null
val job =
lifecycleOwner.lifecycleScope.launch {
launch {
val lifecycleObserver =
object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
if (showLoadingAnimation) {
if (loadingAnimation == null) {
animationState =
animationStateViewModel?.getAnimationState(
viewModel.screen
)
loadingImageDrawable = animationState?.drawable
// TODO (b/290054874): investigate why app restarts twice
// The lines below are a workaround for the issue of
// wallpaper picker lifecycle restarting twice after a
// config change; because of this, on second start, saved
// instance state would always return null. Instead we would
// like the saved instance state on the first restart to
// pass through to the second.
animationTimeToRestore = animationState?.time
animationTransitionProgress =
animationState?.transitionProgress
animationColorToRestore = animationState?.color
// a null drawable means the loading animation should not
// be played
loadingImageDrawable?.let {
loadingView.setImageDrawable(it)
loadingAnimation =
LoadingAnimation(
loadingView,
LoadingAnimation.RevealType.CIRCULAR,
LoadingAnimation.TIME_OUT_DURATION_MS
)
}
}
}
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
if (isPageTransitionsFeatureEnabled) {
disposableHandle?.dispose()
wallpaperConnection?.destroy()
wallpaperConnection = null
}
}
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
animationTimeToRestore =
loadingAnimation?.getElapsedTime() ?: animationTimeToRestore
animationTransitionProgress =
loadingAnimation?.getTransitionProgress()
?: animationTransitionProgress
loadingAnimation?.end()
loadingAnimation = null
// To ensure reveal animation is only played after a theme config
// change from wallpaper/color switch, only save the current loading
// image if this is a configuration change restart and reset to
// null otherwise
animationStateViewModel?.saveAnimationState(
viewModel.screen,
// Check if activity is changing configurations, and check that
// the set of changing configurations does not include screen
// size changes (such as rotation and folding/unfolding device)
// Note: activity.changingConfigurations is not 100% accurate
if (
activity.isChangingConfigurations &&
(activity.changingConfigurations.and(
ActivityInfo.CONFIG_SCREEN_SIZE
) == 0)
) {
AnimationStateViewModel.AnimationState(
loadingImageDrawable,
animationTimeToRestore,
animationTransitionProgress,
animationColorToRestore,
)
} else null
)
wallpaperIsReadyForReveal = false
if (!isPageTransitionsFeatureEnabled) {
disposableHandle?.dispose()
wallpaperConnection?.destroy()
wallpaperConnection = null
}
}
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
wallpaperConnection?.setVisibility(false)
}
}
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
previewSurfaceCallback =
WorkspaceSurfaceHolderCallback(
workspaceSurface,
viewModel.previewUtils,
viewModel.getInitialExtras(),
)
workspaceSurface.holder.addCallback(previewSurfaceCallback)
if (!dimWallpaper) {
workspaceSurface.setZOrderMediaOverlay(true)
}
wallpaperSurfaceCallback =
WallpaperSurfaceCallback(
previewView.context,
previewView,
wallpaperSurface,
CompletableFuture.completedFuture(
WallpaperInfo.ColorInfo(
/* wallpaperColors= */ null,
ResourceUtils.getColorAttr(
previewView.context,
android.R.attr.colorSecondary,
)
)
),
) {
maybeLoadThumbnail(
activity = activity,
wallpaperInfo = wallpaperInfo,
surfaceCallback = wallpaperSurfaceCallback,
offsetToStart = offsetToStart,
onSurfaceViewsReady = surfaceViewsReady,
thumbnailRequested = thumbnailRequested
)
if (showLoadingAnimation) {
val colorAccent =
animationColorToRestore
?: ResourceUtils.getColorAttr(
activity,
android.R.attr.colorAccent
)
val night =
(previewView.resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES)
loadingAnimation?.updateColor(
ColorScheme(seed = colorAccent, darkTheme = night)
)
loadingAnimation?.setupRevealAnimation(
animationTimeToRestore,
animationTransitionProgress
)
val isStaticWallpaper =
wallpaperInfo != null && wallpaperInfo !is LiveWallpaperInfo
wallpaperIsReadyForReveal =
isStaticWallpaper || wallpaperIsReadyForReveal
if (wallpaperIsReadyForReveal) {
loadingAnimation?.playRevealAnimation()
}
}
}
wallpaperSurface.holder.addCallback(wallpaperSurfaceCallback)
if (!dimWallpaper) {
wallpaperSurface.setZOrderMediaOverlay(true)
}
if (!isWallpaperAlwaysVisible) {
wallpaperSurface.visibilityCallback = { visible: Boolean ->
showLivePreview.set(visible)
wallpaperConnection?.setVisibility(showLivePreview.get())
}
}
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
}
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
workspaceSurface.holder.removeCallback(previewSurfaceCallback)
previewSurfaceCallback?.cleanUp()
wallpaperSurface.holder.removeCallback(wallpaperSurfaceCallback)
wallpaperSurfaceCallback?.homeImageWallpaper?.post {
wallpaperSurfaceCallback?.cleanUp()
}
}
launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
var initialWallpaperUpdate = true
viewModel.shouldReloadWallpaper().collect { shouldReload ->
viewModel.getWallpaperInfo(forceReload = false)
// Do not update screen preview on initial update,since the initial
// update results from starting or resuming the activity.
if (initialWallpaperUpdate) {
initialWallpaperUpdate = false
} else if (shouldReload) {
onWallpaperPreviewDirty()
}
}
}
}
launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.wallpaperThumbnail().collect { thumbnail ->
currentWallpaperThumbnail = thumbnail
}
}
}
launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.workspaceUpdateEvents()?.collect {
workspaceSurface.holder.removeCallback(previewSurfaceCallback)
previewSurfaceCallback?.cleanUp()
removeAndReadd(workspaceSurface)
previewSurfaceCallback =
WorkspaceSurfaceHolderCallback(
workspaceSurface,
viewModel.previewUtils,
viewModel.getInitialExtras(),
)
workspaceSurface.holder.addCallback(previewSurfaceCallback)
}
}
}
if (showLoadingAnimation) {
launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.isLoading.collect { isLoading ->
if (isLoading) {
loadingAnimation?.cancel()
// Loading is started, create a new loading animation
// with the current wallpaper as background.
// First, try to get the wallpaper image from
// wallpaperSurfaceCallback, this is the best solution for
// static and live wallpapers but not for creative wallpapers
val wallpaperPreviewImage =
wallpaperSurfaceCallback?.homeImageWallpaper
// If wallpaper drawable was not loaded, and the preview
// drawable is the placeholder color drawable, use the wallpaper
// thumbnail instead: the best solution for creative wallpapers
val animationBackground: Drawable? =
if (wallpaperPreviewImage?.drawable is ColorDrawable) {
currentWallpaperThumbnail?.let { thumbnail ->
BitmapDrawable(activity.resources, thumbnail)
}
?: wallpaperPreviewImage.drawable
} else wallpaperPreviewImage?.drawable
animationBackground?.let {
loadingView.setImageDrawable(animationBackground)
loadingAnimation =
LoadingAnimation(
loadingView,
LoadingAnimation.RevealType.CIRCULAR,
LoadingAnimation.TIME_OUT_DURATION_MS
)
}
loadingImageDrawable = animationBackground
val colorAccent =
ResourceUtils.getColorAttr(
activity,
android.R.attr.colorAccent
)
val night =
(previewView.resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES)
animationColorToRestore = colorAccent
loadingAnimation?.updateColor(
ColorScheme(seed = colorAccent, darkTheme = night)
)
loadingAnimation?.playLoadingAnimation()
}
}
}
}
}
launch {
lifecycleOwner.repeatOnLifecycle(
if (isPageTransitionsFeatureEnabled) {
Lifecycle.State.STARTED
} else {
Lifecycle.State.RESUMED
}
) {
lifecycleOwner.lifecycleScope.launch {
wallpaperInfo = viewModel.getWallpaperInfo(forceReload = false)
maybeLoadThumbnail(
activity = activity,
wallpaperInfo = wallpaperInfo,
surfaceCallback = wallpaperSurfaceCallback,
offsetToStart = offsetToStart,
onSurfaceViewsReady = surfaceViewsReady,
thumbnailRequested = thumbnailRequested
)
if (showLoadingAnimation && wallpaperInfo !is LiveWallpaperInfo) {
loadingAnimation?.playRevealAnimation()
}
(wallpaperInfo as? LiveWallpaperInfo)?.let { liveWallpaperInfo ->
if (isPageTransitionsFeatureEnabled) {
disposableHandle?.dispose()
wallpaperConnection?.destroy()
wallpaperConnection = null
}
val connection =
wallpaperConnection
?: createWallpaperConnection(
liveWallpaperInfo,
previewView,
viewModel,
wallpaperSurface,
mirrorSurface,
viewModel.screen
) {
surfaceViewsReady()
if (showLoadingAnimation) {
wallpaperIsReadyForReveal = true
loadingAnimation?.playRevealAnimation()
}
}
.also { wallpaperConnection = it }
if (!previewView.isAttachedToWindow) {
// Sometimes the service gets connected before the view
// is valid.
// TODO(b/284233455): investigate why and remove this workaround
val listener =
object : OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
connection.connect()
connection.setVisibility(showLivePreview.get())
previewView.removeOnAttachStateChangeListener(this)
}
override fun onViewDetachedFromWindow(v: View) {
// Do nothing
}
}
previewView.addOnAttachStateChangeListener(listener)
disposableHandle = DisposableHandle {
previewView.removeOnAttachStateChangeListener(listener)
}
} else {
connection.connect()
connection.setVisibility(showLivePreview.get())
}
}
}
}
}
}
return object : Binding {
override fun sendMessage(id: Int, args: Bundle) {
previewSurfaceCallback?.send(id, args)
}
override fun destroy() {
job.cancel()
// We want to remove the SurfaceView from its parent and add it back. This causes
// the hierarchy to treat the SurfaceView as "dirty" which will cause it to render
// itself anew the next time the bind function is invoked.
removeAndReadd(workspaceSurface)
}
override fun surface(): SurfaceView {
return wallpaperSurface
}
}
}
private fun createWallpaperConnection(
liveWallpaperInfo: LiveWallpaperInfo,
previewView: CardView,
viewModel: ScreenPreviewViewModel,
wallpaperSurface: SurfaceView,
mirrorSurface: SurfaceView?,
screen: CustomizationSections.Screen,
onEngineShown: () -> Unit
) =
WallpaperConnection(
Intent(WallpaperService.SERVICE_INTERFACE).apply {
setClassName(
liveWallpaperInfo.wallpaperComponent.packageName,
liveWallpaperInfo.wallpaperComponent.serviceName
)
},
previewView.context,
object : WallpaperConnection.WallpaperConnectionListener {
override fun onWallpaperColorsChanged(colors: WallpaperColors?, displayId: Int) {
viewModel.onWallpaperColorsChanged(colors)
}
override fun onEngineShown() {
onEngineShown()
}
},
wallpaperSurface,
mirrorSurface,
screen.toFlag(),
WallpaperConnection.WhichPreview.PREVIEW_CURRENT
)
private fun removeAndReadd(view: View) {
(view.parent as? ViewGroup)?.let { parent ->
val indexInParent = parent.indexOfChild(view)
if (indexInParent >= 0) {
parent.removeView(view)
parent.addView(view, indexInParent)
}
}
}
private fun maybeLoadThumbnail(
activity: Activity,
wallpaperInfo: WallpaperInfo?,
surfaceCallback: WallpaperSurfaceCallback?,
offsetToStart: Boolean,
onSurfaceViewsReady: () -> Unit,
thumbnailRequested: AtomicBoolean
) {
if (wallpaperInfo == null || surfaceCallback == null) {
return
}
val imageView = surfaceCallback.homeImageWallpaper
val thumbAsset: Asset = wallpaperInfo.getThumbAsset(activity)
if (imageView != null && imageView.drawable == null) {
if (!thumbnailRequested.compareAndSet(false, true)) {
return
}
// Respect offsetToStart only for CurrentWallpaperAssetVN otherwise true.
BitmapCachingAsset(activity, thumbAsset)
.loadPreviewImage(
activity,
imageView,
ResourceUtils.getColorAttr(activity, android.R.attr.colorSecondary),
/* offsetToStart= */ thumbAsset !is CurrentWallpaperAsset || offsetToStart
)
if (wallpaperInfo !is LiveWallpaperInfo) {
imageView.addOnLayoutChangeListener(
object : View.OnLayoutChangeListener {
override fun onLayoutChange(
v: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
v?.removeOnLayoutChangeListener(this)
onSurfaceViewsReady()
}
}
)
}
}
}
}