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