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