• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  *
16  */
17 
18 package com.android.wallpaper.picker.customization.ui.binder
19 
20 import android.app.Activity
21 import android.app.WallpaperColors
22 import android.content.Intent
23 import android.content.pm.ActivityInfo
24 import android.content.res.Configuration
25 import android.graphics.Bitmap
26 import android.graphics.Color
27 import android.graphics.drawable.BitmapDrawable
28 import android.graphics.drawable.ColorDrawable
29 import android.graphics.drawable.Drawable
30 import android.os.Bundle
31 import android.service.wallpaper.WallpaperService
32 import android.view.SurfaceView
33 import android.view.View
34 import android.view.View.OnAttachStateChangeListener
35 import android.view.ViewGroup
36 import android.widget.ImageView
37 import androidx.cardview.widget.CardView
38 import androidx.core.view.isVisible
39 import androidx.lifecycle.DefaultLifecycleObserver
40 import androidx.lifecycle.Lifecycle
41 import androidx.lifecycle.LifecycleOwner
42 import androidx.lifecycle.lifecycleScope
43 import androidx.lifecycle.repeatOnLifecycle
44 import com.android.systemui.monet.ColorScheme
45 import com.android.wallpaper.R
46 import com.android.wallpaper.asset.Asset
47 import com.android.wallpaper.asset.BitmapCachingAsset
48 import com.android.wallpaper.asset.CurrentWallpaperAssetVN
49 import com.android.wallpaper.config.BaseFlags
50 import com.android.wallpaper.model.LiveWallpaperInfo
51 import com.android.wallpaper.model.WallpaperInfo
52 import com.android.wallpaper.module.CustomizationSections
53 import com.android.wallpaper.picker.WorkspaceSurfaceHolderCallback
54 import com.android.wallpaper.picker.customization.animation.view.LoadingAnimation
55 import com.android.wallpaper.picker.customization.ui.view.WallpaperSurfaceView
56 import com.android.wallpaper.picker.customization.ui.viewmodel.AnimationStateViewModel
57 import com.android.wallpaper.picker.customization.ui.viewmodel.ScreenPreviewViewModel
58 import com.android.wallpaper.util.ResourceUtils
59 import com.android.wallpaper.util.WallpaperConnection
60 import com.android.wallpaper.util.WallpaperSurfaceCallback
61 import java.util.concurrent.CompletableFuture
62 import java.util.concurrent.atomic.AtomicBoolean
63 import kotlinx.coroutines.launch
64 
65 /**
66  * Binds between view and view-model for rendering the preview of the home screen or the lock
67  * screen.
68  */
69 object ScreenPreviewBinder {
70     interface Binding {
71         fun sendMessage(
72             id: Int,
73             args: Bundle = Bundle.EMPTY,
74         )
75         fun destroy()
76         fun surface(): SurfaceView
77     }
78 
79     /**
80      * Binds the view to the given [viewModel].
81      *
82      * Note that if [dimWallpaper] is `true`, the wallpaper will be dimmed (to help highlight
83      * something that is changing on top of the wallpaper, for example, the lock screen shortcuts or
84      * the clock).
85      */
86     // TODO (b/274443705): incorporate color picker to allow preview loading on color change
87     // TODO (b/274443705): make loading animation more continuous on reveal
88     // TODO (b/274443705): adjust for better timing on animation reveal
89     @JvmStatic
90     fun bind(
91         activity: Activity,
92         previewView: CardView,
93         viewModel: ScreenPreviewViewModel,
94         lifecycleOwner: LifecycleOwner,
95         offsetToStart: Boolean,
96         dimWallpaper: Boolean = false,
97         onWallpaperPreviewDirty: () -> Unit,
98         onWorkspacePreviewDirty: () -> Unit = {},
99         animationStateViewModel: AnimationStateViewModel? = null,
100         isWallpaperAlwaysVisible: Boolean = true,
101         mirrorSurface: SurfaceView? = null,
102     ): Binding {
103         val workspaceSurface: SurfaceView = previewView.requireViewById(R.id.workspace_surface)
104         val wallpaperSurface: WallpaperSurfaceView =
105             previewView.requireViewById(R.id.wallpaper_surface)
106         val thumbnailRequested = AtomicBoolean(false)
107         // Tracks whether the live preview should be shown, since a) visibility updates may arrive
108         // before the engine is ready, and b) we need this state for onResume
109         // TODO(b/287618705) Remove this
110         val showLivePreview = AtomicBoolean(isWallpaperAlwaysVisible)
111         val fixedWidthDisplayFrameLayout = previewView.parent as? View
112         val screenPreviewClickView = fixedWidthDisplayFrameLayout?.parent as? View
113         // Set the content description on the parent view
114         screenPreviewClickView?.contentDescription =
115             activity.resources.getString(viewModel.previewContentDescription)
116         fixedWidthDisplayFrameLayout?.importantForAccessibility =
117             View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
118 
119         // This ensures that we do not announce the time multiple times
120         previewView.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
121         var wallpaperIsReadyForReveal = false
122         val surfaceViewsReady = {
123             wallpaperSurface.setBackgroundColor(Color.TRANSPARENT)
124             workspaceSurface.visibility = View.VISIBLE
125         }
126         wallpaperSurface.setZOrderOnTop(false)
127 
128         val flags = BaseFlags.get()
129         val isPageTransitionsFeatureEnabled = flags.isPageTransitionsFeatureEnabled(activity)
130 
131         val showLoadingAnimation =
132             flags.isPreviewLoadingAnimationEnabled(activity.applicationContext)
133         var loadingAnimation: LoadingAnimation? = null
134         val loadingView: ImageView = previewView.requireViewById(R.id.loading_view)
135 
136         if (dimWallpaper) {
137             previewView.requireViewById<View>(R.id.wallpaper_dimming_scrim).isVisible = true
138             workspaceSurface.setZOrderOnTop(true)
139         }
140 
141         previewView.radius =
142             previewView.resources.getDimension(R.dimen.wallpaper_picker_entry_card_corner_radius)
143 
144         previewView.setOnClickListener { viewModel.onPreviewClicked?.invoke() }
145 
146         var previewSurfaceCallback: WorkspaceSurfaceHolderCallback? = null
147         var wallpaperSurfaceCallback: WallpaperSurfaceCallback? = null
148         var wallpaperConnection: WallpaperConnection? = null
149         var wallpaperInfo: WallpaperInfo? = null
150         var animationState: AnimationStateViewModel.AnimationState? = null
151         var loadingImageDrawable: Drawable? = null
152         var animationTimeToRestore: Long? = null
153         var animationTransitionProgress: Float? = null
154         var animationColorToRestore: Int? = null
155         var currentWallpaperThumbnail: Bitmap? = null
156 
157         val job =
158             lifecycleOwner.lifecycleScope.launch {
159                 launch {
160                     val lifecycleObserver =
161                         object : DefaultLifecycleObserver {
162                             override fun onStart(owner: LifecycleOwner) {
163                                 super.onStart(owner)
164                                 if (showLoadingAnimation) {
165                                     if (loadingAnimation == null) {
166                                         animationState =
167                                             animationStateViewModel?.getAnimationState(
168                                                 viewModel.screen
169                                             )
170                                         loadingImageDrawable = animationState?.drawable
171                                         // TODO (b/290054874): investigate why app restarts twice
172                                         // The lines below are a workaround for the issue of
173                                         // wallpaper picker lifecycle restarting twice after a
174                                         // config change; because of this, on second start, saved
175                                         // instance state would always return null. Instead we would
176                                         // like the saved instance state on the first restart to
177                                         // pass through to the second.
178                                         animationTimeToRestore = animationState?.time
179                                         animationTransitionProgress =
180                                             animationState?.transitionProgress
181                                         animationColorToRestore = animationState?.color
182                                         // a null drawable means the loading animation should not
183                                         // be played
184                                         loadingImageDrawable?.let {
185                                             loadingView.setImageDrawable(it)
186                                             loadingAnimation =
187                                                 LoadingAnimation(
188                                                     loadingView,
189                                                     LoadingAnimation.RevealType.CIRCULAR,
190                                                     LoadingAnimation.TIME_OUT_DURATION_MS
191                                                 )
192                                         }
193                                     }
194                                 }
195                             }
196 
197                             override fun onDestroy(owner: LifecycleOwner) {
198                                 super.onDestroy(owner)
199                                 if (isPageTransitionsFeatureEnabled) {
200                                     wallpaperConnection?.destroy()
201                                     wallpaperConnection = null
202                                 }
203                             }
204 
205                             override fun onStop(owner: LifecycleOwner) {
206                                 super.onStop(owner)
207                                 animationTimeToRestore =
208                                     loadingAnimation?.getElapsedTime() ?: animationTimeToRestore
209                                 animationTransitionProgress =
210                                     loadingAnimation?.getTransitionProgress()
211                                         ?: animationTransitionProgress
212                                 loadingAnimation?.end()
213                                 loadingAnimation = null
214                                 // To ensure reveal animation is only played after a theme config
215                                 // change from wallpaper/color switch, only save the current loading
216                                 // image if this is a configuration change restart and reset to
217                                 // null otherwise
218                                 animationStateViewModel?.saveAnimationState(
219                                     viewModel.screen,
220                                     // Check if activity is changing configurations, and check that
221                                     // the set of changing configurations does not include screen
222                                     // size changes (such as rotation and folding/unfolding device)
223                                     // Note: activity.changingConfigurations is not 100% accurate
224                                     if (
225                                         activity.isChangingConfigurations &&
226                                             (activity.changingConfigurations.and(
227                                                 ActivityInfo.CONFIG_SCREEN_SIZE
228                                             ) == 0)
229                                     ) {
230                                         AnimationStateViewModel.AnimationState(
231                                             loadingImageDrawable,
232                                             animationTimeToRestore,
233                                             animationTransitionProgress,
234                                             animationColorToRestore,
235                                         )
236                                     } else null
237                                 )
238                                 wallpaperIsReadyForReveal = false
239                                 if (!isPageTransitionsFeatureEnabled) {
240                                     wallpaperConnection?.destroy()
241                                     wallpaperConnection = null
242                                 }
243                             }
244 
245                             override fun onPause(owner: LifecycleOwner) {
246                                 super.onPause(owner)
247                                 wallpaperConnection?.setVisibility(false)
248                             }
249                         }
250 
251                     lifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
252                         previewSurfaceCallback =
253                             WorkspaceSurfaceHolderCallback(
254                                 workspaceSurface,
255                                 viewModel.previewUtils,
256                                 viewModel.getInitialExtras(),
257                             )
258                         workspaceSurface.holder.addCallback(previewSurfaceCallback)
259                         if (!dimWallpaper) {
260                             workspaceSurface.setZOrderMediaOverlay(true)
261                         }
262 
263                         wallpaperSurfaceCallback =
264                             WallpaperSurfaceCallback(
265                                 previewView.context,
266                                 previewView,
267                                 wallpaperSurface,
268                                 CompletableFuture.completedFuture(
269                                     WallpaperInfo.ColorInfo(
270                                         /* wallpaperColors= */ null,
271                                         ResourceUtils.getColorAttr(
272                                             previewView.context,
273                                             android.R.attr.colorSecondary,
274                                         )
275                                     )
276                                 ),
277                             ) {
278                                 maybeLoadThumbnail(
279                                     activity = activity,
280                                     wallpaperInfo = wallpaperInfo,
281                                     surfaceCallback = wallpaperSurfaceCallback,
282                                     offsetToStart = offsetToStart,
283                                     onSurfaceViewsReady = surfaceViewsReady,
284                                     thumbnailRequested = thumbnailRequested
285                                 )
286                                 if (showLoadingAnimation) {
287                                     val colorAccent =
288                                         animationColorToRestore
289                                             ?: ResourceUtils.getColorAttr(
290                                                 activity,
291                                                 android.R.attr.colorAccent
292                                             )
293                                     val night =
294                                         (previewView.resources.configuration.uiMode and
295                                             Configuration.UI_MODE_NIGHT_MASK ==
296                                             Configuration.UI_MODE_NIGHT_YES)
297                                     loadingAnimation?.updateColor(
298                                         ColorScheme(seed = colorAccent, darkTheme = night)
299                                     )
300                                     loadingAnimation?.setupRevealAnimation(
301                                         animationTimeToRestore,
302                                         animationTransitionProgress
303                                     )
304                                     val isStaticWallpaper =
305                                         wallpaperInfo != null && wallpaperInfo !is LiveWallpaperInfo
306                                     wallpaperIsReadyForReveal =
307                                         isStaticWallpaper || wallpaperIsReadyForReveal
308                                     if (wallpaperIsReadyForReveal) {
309                                         loadingAnimation?.playRevealAnimation()
310                                     }
311                                 }
312                             }
313                         wallpaperSurface.holder.addCallback(wallpaperSurfaceCallback)
314                         if (!dimWallpaper) {
315                             wallpaperSurface.setZOrderMediaOverlay(true)
316                         }
317 
318                         if (!isWallpaperAlwaysVisible) {
319                             wallpaperSurface.visibilityCallback = { visible: Boolean ->
320                                 showLivePreview.set(visible)
321                                 wallpaperConnection?.setVisibility(showLivePreview.get())
322                             }
323                         }
324 
325                         lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
326                     }
327 
328                     lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
329                     workspaceSurface.holder.removeCallback(previewSurfaceCallback)
330                     previewSurfaceCallback?.cleanUp()
331                     wallpaperSurface.holder.removeCallback(wallpaperSurfaceCallback)
332                     wallpaperSurfaceCallback?.homeImageWallpaper?.post {
333                         wallpaperSurfaceCallback?.cleanUp()
334                     }
335                 }
336 
337                 launch {
338                     lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
339                         var initialWallpaperUpdate = true
340                         viewModel.shouldReloadWallpaper().collect { shouldReload ->
341                             viewModel.getWallpaperInfo(forceReload = false)
342                             // Do not update screen preview on initial update,since the initial
343                             // update results from starting or resuming the activity.
344                             if (initialWallpaperUpdate) {
345                                 initialWallpaperUpdate = false
346                             } else if (shouldReload) {
347                                 onWallpaperPreviewDirty()
348                             }
349                         }
350                     }
351                 }
352 
353                 launch {
354                     lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
355                         viewModel.wallpaperThumbnail().collect { thumbnail ->
356                             currentWallpaperThumbnail = thumbnail
357                         }
358                     }
359                 }
360 
361                 launch {
362                     lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
363                         var initialWorkspaceUpdate = true
364                         viewModel.workspaceUpdateEvents()?.collect {
365                             if (initialWorkspaceUpdate) {
366                                 initialWorkspaceUpdate = false
367                             } else {
368                                 onWorkspacePreviewDirty()
369                             }
370                         }
371                     }
372                 }
373 
374                 if (showLoadingAnimation) {
375                     launch {
376                         lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
377                             viewModel.isLoading.collect { isLoading ->
378                                 if (isLoading) {
379                                     loadingAnimation?.cancel()
380 
381                                     // Loading is started, create a new loading animation
382                                     // with the current wallpaper as background.
383                                     // First, try to get the wallpaper image from
384                                     // wallpaperSurfaceCallback, this is the best solution for
385                                     // static and live wallpapers but not for creative wallpapers
386                                     val wallpaperPreviewImage =
387                                         wallpaperSurfaceCallback?.homeImageWallpaper
388                                     // If wallpaper drawable was not loaded, and the preview
389                                     // drawable is the placeholder color drawable, use the wallpaper
390                                     // thumbnail instead: the best solution for creative wallpapers
391                                     val animationBackground: Drawable? =
392                                         if (wallpaperPreviewImage?.drawable is ColorDrawable) {
393                                             currentWallpaperThumbnail?.let { thumbnail ->
394                                                 BitmapDrawable(activity.resources, thumbnail)
395                                             }
396                                                 ?: wallpaperPreviewImage.drawable
397                                         } else wallpaperPreviewImage?.drawable
398                                     animationBackground?.let {
399                                         loadingView.setImageDrawable(animationBackground)
400                                         loadingAnimation =
401                                             LoadingAnimation(
402                                                 loadingView,
403                                                 LoadingAnimation.RevealType.CIRCULAR,
404                                                 LoadingAnimation.TIME_OUT_DURATION_MS
405                                             )
406                                     }
407                                     loadingImageDrawable = animationBackground
408                                     val colorAccent =
409                                         ResourceUtils.getColorAttr(
410                                             activity,
411                                             android.R.attr.colorAccent
412                                         )
413                                     val night =
414                                         (previewView.resources.configuration.uiMode and
415                                             Configuration.UI_MODE_NIGHT_MASK ==
416                                             Configuration.UI_MODE_NIGHT_YES)
417                                     animationColorToRestore = colorAccent
418                                     loadingAnimation?.updateColor(
419                                         ColorScheme(seed = colorAccent, darkTheme = night)
420                                     )
421                                     loadingAnimation?.playLoadingAnimation()
422                                 }
423                             }
424                         }
425                     }
426                 }
427 
428                 launch {
429                     lifecycleOwner.repeatOnLifecycle(
430                         if (isPageTransitionsFeatureEnabled) {
431                             Lifecycle.State.STARTED
432                         } else {
433                             Lifecycle.State.RESUMED
434                         }
435                     ) {
436                         lifecycleOwner.lifecycleScope.launch {
437                             wallpaperInfo = viewModel.getWallpaperInfo(forceReload = false)
438                             maybeLoadThumbnail(
439                                 activity = activity,
440                                 wallpaperInfo = wallpaperInfo,
441                                 surfaceCallback = wallpaperSurfaceCallback,
442                                 offsetToStart = offsetToStart,
443                                 onSurfaceViewsReady = surfaceViewsReady,
444                                 thumbnailRequested = thumbnailRequested
445                             )
446                             if (showLoadingAnimation && wallpaperInfo !is LiveWallpaperInfo) {
447                                 loadingAnimation?.playRevealAnimation()
448                             }
449                             (wallpaperInfo as? LiveWallpaperInfo)?.let { liveWallpaperInfo ->
450                                 if (isPageTransitionsFeatureEnabled) {
451                                     wallpaperConnection?.destroy()
452                                     wallpaperConnection = null
453                                 }
454                                 val connection =
455                                     wallpaperConnection
456                                         ?: createWallpaperConnection(
457                                                 liveWallpaperInfo,
458                                                 previewView,
459                                                 viewModel,
460                                                 wallpaperSurface,
461                                                 mirrorSurface,
462                                                 viewModel.screen
463                                             ) {
464                                                 surfaceViewsReady()
465                                                 if (showLoadingAnimation) {
466                                                     wallpaperIsReadyForReveal = true
467                                                     loadingAnimation?.playRevealAnimation()
468                                                 }
469                                             }
470                                             .also { wallpaperConnection = it }
471                                 if (!previewView.isAttachedToWindow) {
472                                     // Sometimes the service gets connected before the view
473                                     // is valid.
474                                     // TODO(b/284233455): investigate why and remove this workaround
475                                     previewView.addOnAttachStateChangeListener(
476                                         object : OnAttachStateChangeListener {
477                                             override fun onViewAttachedToWindow(v: View?) {
478                                                 connection.connect()
479                                                 connection.setVisibility(showLivePreview.get())
480                                                 previewView.removeOnAttachStateChangeListener(this)
481                                             }
482 
483                                             override fun onViewDetachedFromWindow(v: View?) {
484                                                 // Do nothing
485                                             }
486                                         }
487                                     )
488                                 } else {
489                                     connection.connect()
490                                     connection.setVisibility(showLivePreview.get())
491                                 }
492                             }
493                         }
494                     }
495                 }
496             }
497 
498         return object : Binding {
499             override fun sendMessage(id: Int, args: Bundle) {
500                 previewSurfaceCallback?.send(id, args)
501             }
502 
503             override fun destroy() {
504                 job.cancel()
505                 // We want to remove the SurfaceView from its parent and add it back. This causes
506                 // the hierarchy to treat the SurfaceView as "dirty" which will cause it to render
507                 // itself anew the next time the bind function is invoked.
508                 removeAndReadd(workspaceSurface)
509             }
510 
511             override fun surface(): SurfaceView {
512                 return wallpaperSurface
513             }
514         }
515     }
516 
517     private fun createWallpaperConnection(
518         liveWallpaperInfo: LiveWallpaperInfo,
519         previewView: CardView,
520         viewModel: ScreenPreviewViewModel,
521         wallpaperSurface: SurfaceView,
522         mirrorSurface: SurfaceView?,
523         screen: CustomizationSections.Screen,
524         onEngineShown: () -> Unit
525     ) =
526         WallpaperConnection(
527             Intent(WallpaperService.SERVICE_INTERFACE).apply {
528                 setClassName(
529                     liveWallpaperInfo.wallpaperComponent.packageName,
530                     liveWallpaperInfo.wallpaperComponent.serviceName
531                 )
532             },
533             previewView.context,
534             object : WallpaperConnection.WallpaperConnectionListener {
535                 override fun onWallpaperColorsChanged(colors: WallpaperColors?, displayId: Int) {
536                     viewModel.onWallpaperColorsChanged(colors)
537                 }
538 
539                 override fun onEngineShown() {
540                     onEngineShown()
541                 }
542             },
543             wallpaperSurface,
544             mirrorSurface,
545             screen.toFlag(),
546             WallpaperConnection.WHICH_PREVIEW.PREVIEW_CURRENT
547         )
548 
549     private fun removeAndReadd(view: View) {
550         (view.parent as? ViewGroup)?.let { parent ->
551             val indexInParent = parent.indexOfChild(view)
552             if (indexInParent >= 0) {
553                 parent.removeView(view)
554                 parent.addView(view, indexInParent)
555             }
556         }
557     }
558 
559     private fun maybeLoadThumbnail(
560         activity: Activity,
561         wallpaperInfo: WallpaperInfo?,
562         surfaceCallback: WallpaperSurfaceCallback?,
563         offsetToStart: Boolean,
564         onSurfaceViewsReady: () -> Unit,
565         thumbnailRequested: AtomicBoolean
566     ) {
567         if (wallpaperInfo == null || surfaceCallback == null) {
568             return
569         }
570 
571         val imageView = surfaceCallback.homeImageWallpaper
572         val thumbAsset: Asset = wallpaperInfo.getThumbAsset(activity)
573         if (imageView != null && imageView.drawable == null) {
574             if (!thumbnailRequested.compareAndSet(false, true)) {
575                 return
576             }
577             // Respect offsetToStart only for CurrentWallpaperAssetVN otherwise true.
578             BitmapCachingAsset(activity, thumbAsset)
579                 .loadPreviewImage(
580                     activity,
581                     imageView,
582                     ResourceUtils.getColorAttr(activity, android.R.attr.colorSecondary),
583                     /* offsetToStart= */ thumbAsset !is CurrentWallpaperAssetVN || offsetToStart
584                 )
585             if (wallpaperInfo !is LiveWallpaperInfo) {
586                 imageView.addOnLayoutChangeListener(
587                     object : View.OnLayoutChangeListener {
588                         override fun onLayoutChange(
589                             v: View?,
590                             left: Int,
591                             top: Int,
592                             right: Int,
593                             bottom: Int,
594                             oldLeft: Int,
595                             oldTop: Int,
596                             oldRight: Int,
597                             oldBottom: Int
598                         ) {
599                             v?.removeOnLayoutChangeListener(this)
600                             onSurfaceViewsReady()
601                         }
602                     }
603                 )
604             }
605         }
606     }
607 }
608