1 /* <lambda>null2 * Copyright (C) 2023 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 package com.android.wallpaper.picker.preview.ui.binder 17 18 import android.annotation.SuppressLint 19 import android.content.Context 20 import android.graphics.Point 21 import android.os.Bundle 22 import android.view.Gravity 23 import android.view.LayoutInflater 24 import android.view.SurfaceHolder 25 import android.view.SurfaceView 26 import android.view.View 27 import android.widget.FrameLayout 28 import android.widget.ImageView 29 import androidx.cardview.widget.CardView 30 import androidx.core.view.doOnLayout 31 import androidx.core.view.isVisible 32 import androidx.lifecycle.Lifecycle 33 import androidx.lifecycle.LifecycleOwner 34 import androidx.lifecycle.lifecycleScope 35 import androidx.lifecycle.repeatOnLifecycle 36 import androidx.transition.Transition 37 import androidx.transition.TransitionListenerAdapter 38 import com.android.wallpaper.R 39 import com.android.wallpaper.model.wallpaper.DeviceDisplayType 40 import com.android.wallpaper.picker.TouchForwardingLayout 41 import com.android.wallpaper.picker.data.WallpaperModel 42 import com.android.wallpaper.picker.preview.shared.model.CropSizeModel 43 import com.android.wallpaper.picker.preview.shared.model.FullPreviewCropModel 44 import com.android.wallpaper.picker.preview.ui.util.SubsamplingScaleImageViewUtil.setOnNewCropListener 45 import com.android.wallpaper.picker.preview.ui.util.SurfaceViewUtil 46 import com.android.wallpaper.picker.preview.ui.util.SurfaceViewUtil.attachView 47 import com.android.wallpaper.picker.preview.ui.view.FullPreviewFrameLayout 48 import com.android.wallpaper.picker.preview.ui.viewmodel.WallpaperPreviewViewModel 49 import com.android.wallpaper.util.DisplayUtils 50 import com.android.wallpaper.util.RtlUtils.isRtl 51 import com.android.wallpaper.util.WallpaperCropUtils 52 import com.android.wallpaper.util.wallpaperconnection.WallpaperConnectionUtils 53 import com.android.wallpaper.util.wallpaperconnection.WallpaperConnectionUtils.shouldEnforceSingleEngine 54 import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView 55 import java.lang.Integer.min 56 import kotlin.math.max 57 import kotlinx.coroutines.DisposableHandle 58 import kotlinx.coroutines.Job 59 import kotlinx.coroutines.launch 60 61 /** Binds wallpaper preview surface view and its view models. */ 62 object FullWallpaperPreviewBinder { 63 64 fun bind( 65 applicationContext: Context, 66 view: View, 67 viewModel: WallpaperPreviewViewModel, 68 transition: Transition?, 69 displayUtils: DisplayUtils, 70 lifecycleOwner: LifecycleOwner, 71 savedInstanceState: Bundle?, 72 isFirstBinding: Boolean, 73 onWallpaperLoaded: ((Boolean) -> Unit)? = null, 74 ) { 75 val wallpaperPreviewCrop: FullPreviewFrameLayout = 76 view.requireViewById(R.id.wallpaper_preview_crop) 77 val previewCard: CardView = view.requireViewById(R.id.preview_card) 78 val scrimView: View = view.requireViewById(R.id.preview_scrim) 79 var transitionDisposableHandle: DisposableHandle? = null 80 val mediumAnimTimeMs = 81 view.resources.getInteger(android.R.integer.config_mediumAnimTime).toLong() 82 lifecycleOwner.lifecycleScope.launch { 83 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 84 viewModel.fullWallpaper.collect { (_, _, displaySize, _) -> 85 val currentSize = displayUtils.getRealSize(checkNotNull(view.context.display)) 86 wallpaperPreviewCrop.setCurrentAndTargetDisplaySize( 87 currentSize, 88 displaySize, 89 ) 90 91 val setFinalPreviewCardRadiusAndEndLoading = { isWallpaperFullScreen: Boolean -> 92 if (isWallpaperFullScreen) { 93 previewCard.radius = 0f 94 } 95 scrimView.isVisible = isWallpaperFullScreen 96 onWallpaperLoaded?.invoke(isWallpaperFullScreen) 97 } 98 val isPreviewingFullScreen = displaySize == currentSize 99 if (transition == null || savedInstanceState != null) { 100 setFinalPreviewCardRadiusAndEndLoading(isPreviewingFullScreen) 101 } else { 102 transitionDisposableHandle?.dispose() 103 val listener = 104 object : TransitionListenerAdapter() { 105 override fun onTransitionStart(transition: Transition) { 106 super.onTransitionStart(transition) 107 if (isPreviewingFullScreen) { 108 scrimView.isVisible = true 109 scrimView.alpha = 0f 110 scrimView 111 .animate() 112 .alpha(1f) 113 .setDuration(mediumAnimTimeMs) 114 .start() 115 } 116 } 117 118 override fun onTransitionEnd(transition: Transition) { 119 super.onTransitionEnd(transition) 120 setFinalPreviewCardRadiusAndEndLoading(isPreviewingFullScreen) 121 } 122 } 123 transition.addListener(listener) 124 transitionDisposableHandle = DisposableHandle { 125 listener.let { transition.removeListener(it) } 126 } 127 } 128 } 129 } 130 transitionDisposableHandle?.dispose() 131 } 132 val surfaceView: SurfaceView = view.requireViewById(R.id.wallpaper_surface) 133 val surfaceTouchForwardingLayout: TouchForwardingLayout = 134 view.requireViewById(R.id.touch_forwarding_layout) 135 136 if (displayUtils.hasMultiInternalDisplays()) { 137 lifecycleOwner.lifecycleScope.launch { 138 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 139 viewModel.fullPreviewConfigViewModel.collect { fullPreviewConfigViewModel -> 140 val deviceDisplayType = fullPreviewConfigViewModel?.deviceDisplayType 141 val descriptionResourceId = 142 when (deviceDisplayType) { 143 DeviceDisplayType.FOLDED -> R.string.folded_device_state_description 144 else -> R.string.unfolded_device_state_description 145 } 146 val descriptionString = 147 surfaceTouchForwardingLayout.context.getString(descriptionResourceId) 148 surfaceTouchForwardingLayout.contentDescription = 149 surfaceTouchForwardingLayout.context.getString( 150 R.string.preview_screen_description_editable, 151 descriptionString 152 ) 153 } 154 } 155 } 156 } else { 157 surfaceTouchForwardingLayout.contentDescription = 158 surfaceTouchForwardingLayout.context.getString( 159 R.string.preview_screen_description_editable, 160 "" 161 ) 162 } 163 164 var surfaceCallback: SurfaceViewUtil.SurfaceCallback? = null 165 lifecycleOwner.lifecycleScope.launch { 166 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { 167 surfaceCallback = 168 bindSurface( 169 applicationContext = applicationContext, 170 surfaceView = surfaceView, 171 surfaceTouchForwardingLayout = surfaceTouchForwardingLayout, 172 viewModel = viewModel, 173 lifecycleOwner = lifecycleOwner, 174 isFirstBinding = isFirstBinding, 175 ) 176 surfaceView.setZOrderMediaOverlay(true) 177 surfaceView.holder.addCallback(surfaceCallback) 178 } 179 // When OnDestroy, release the surface 180 surfaceCallback?.let { 181 surfaceView.holder.removeCallback(it) 182 surfaceCallback = null 183 } 184 } 185 } 186 187 /** 188 * Create a surface callback that binds the surface when surface created. Note that we return 189 * the surface callback reference so that we can remove the callback from the surface when the 190 * screen is destroyed. 191 */ 192 private fun bindSurface( 193 applicationContext: Context, 194 surfaceView: SurfaceView, 195 surfaceTouchForwardingLayout: TouchForwardingLayout, 196 viewModel: WallpaperPreviewViewModel, 197 lifecycleOwner: LifecycleOwner, 198 isFirstBinding: Boolean, 199 ): SurfaceViewUtil.SurfaceCallback { 200 return object : SurfaceViewUtil.SurfaceCallback { 201 202 var job: Job? = null 203 var surfaceOrigWidth: Int? = null 204 var surfaceOrigHeight: Int? = null 205 206 // Suppress lint warning for setting on touch listener to a live wallpaper surface view. 207 // This is because the touch effect on a live wallpaper is purely visual, instead of 208 // functional. The effect can be different for different live wallpapers. 209 @SuppressLint("ClickableViewAccessibility") 210 override fun surfaceCreated(holder: SurfaceHolder) { 211 job = 212 lifecycleOwner.lifecycleScope.launch { 213 viewModel.fullWallpaper.collect { 214 (wallpaper, config, displaySize, allowUserCropping, whichPreview) -> 215 if (wallpaper is WallpaperModel.LiveWallpaperModel) { 216 val engineRenderingConfig = 217 WallpaperConnectionUtils.EngineRenderingConfig( 218 wallpaper.shouldEnforceSingleEngine(), 219 config.deviceDisplayType, 220 viewModel.smallerDisplaySize, 221 displaySize, 222 ) 223 WallpaperConnectionUtils.connect( 224 applicationContext, 225 wallpaper, 226 whichPreview, 227 viewModel.getWallpaperPreviewSource().toFlag(), 228 surfaceView, 229 engineRenderingConfig, 230 isFirstBinding, 231 ) 232 surfaceTouchForwardingLayout.initTouchForwarding(surfaceView) 233 surfaceView.setOnTouchListener { _, event -> 234 lifecycleOwner.lifecycleScope.launch { 235 WallpaperConnectionUtils.dispatchTouchEvent( 236 wallpaper, 237 engineRenderingConfig, 238 event, 239 ) 240 } 241 false 242 } 243 } else if (wallpaper is WallpaperModel.StaticWallpaperModel) { 244 val preview = 245 LayoutInflater.from(applicationContext) 246 .inflate(R.layout.fullscreen_wallpaper_preview, null) 247 adjustSizeAndAttachPreview( 248 applicationContext, 249 surfaceOrigWidth 250 ?: surfaceView.width.also { surfaceOrigWidth = it }, 251 surfaceOrigHeight 252 ?: surfaceView.height.also { surfaceOrigHeight = it }, 253 surfaceView, 254 preview, 255 ) 256 257 val fullResImageView = 258 preview.requireViewById<SubsamplingScaleImageView>( 259 R.id.full_res_image 260 ) 261 fullResImageView.doOnLayout { 262 val imageSize = 263 Point(fullResImageView.width, fullResImageView.height) 264 val cropImageSize = 265 WallpaperCropUtils.calculateCropSurfaceSize( 266 applicationContext.resources, 267 max(imageSize.x, imageSize.y), 268 min(imageSize.x, imageSize.y), 269 imageSize.x, 270 imageSize.y 271 ) 272 fullResImageView.setOnNewCropListener { crop, zoom -> 273 viewModel.staticWallpaperPreviewViewModel 274 .fullPreviewCropModels[displaySize] = 275 FullPreviewCropModel( 276 cropHint = crop, 277 cropSizeModel = 278 CropSizeModel( 279 wallpaperZoom = zoom, 280 hostViewSize = imageSize, 281 cropViewSize = cropImageSize, 282 ), 283 ) 284 } 285 } 286 val lowResImageView = 287 preview.requireViewById<ImageView>(R.id.low_res_image) 288 289 // We do not allow users to pinch to crop if it is a 290 // downloadable wallpaper. 291 if (allowUserCropping) { 292 surfaceTouchForwardingLayout.initTouchForwarding( 293 fullResImageView 294 ) 295 } 296 297 // Bind static wallpaper 298 StaticWallpaperPreviewBinder.bind( 299 lowResImageView = lowResImageView, 300 fullResImageView = fullResImageView, 301 viewModel = viewModel.staticWallpaperPreviewViewModel, 302 displaySize = displaySize, 303 parentCoroutineScope = this, 304 isFullScreen = true, 305 ) 306 } 307 } 308 } 309 } 310 311 override fun surfaceDestroyed(holder: SurfaceHolder) { 312 job?.cancel() 313 job = null 314 // Clean up surface view's on touche listener 315 surfaceTouchForwardingLayout.removeTouchForwarding() 316 surfaceView.setOnTouchListener(null) 317 // Note that we disconnect wallpaper connection for live wallpapers in 318 // WallpaperPreviewActivity's onDestroy(). 319 // This is to reduce multiple times of connecting and disconnecting live 320 // wallpaper services, when going back and forth small and full preview. 321 } 322 } 323 } 324 325 // When showing full screen, we set the parent SurfaceView to be bigger than the image by N 326 // percent (usually 10%) as given by getSystemWallpaperMaximumScale. This ensures that no matter 327 // what scale and pan is set by the user, at least N% of the source image in the preview will be 328 // preserved around the visible crop. This is needed for system zoom out animations. 329 private fun adjustSizeAndAttachPreview( 330 applicationContext: Context, 331 origWidth: Int, 332 origHeight: Int, 333 surfaceView: SurfaceView, 334 preview: View, 335 ) { 336 val scale = WallpaperCropUtils.getSystemWallpaperMaximumScale(applicationContext) 337 338 val width = (origWidth * scale).toInt() 339 val height = (origHeight * scale).toInt() 340 val left = 341 ((origWidth - width) / 2).let { 342 if (isRtl(applicationContext)) { 343 -it 344 } else { 345 it 346 } 347 } 348 val top = (origHeight - height) / 2 349 350 val params = surfaceView.layoutParams 351 params.width = width 352 params.height = height 353 surfaceView.x = left.toFloat() 354 surfaceView.y = top.toFloat() 355 surfaceView.layoutParams = params 356 surfaceView.requestLayout() 357 358 preview.measure( 359 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), 360 View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY) 361 ) 362 preview.layout(0, 0, width, height) 363 364 surfaceView.attachView(preview, width, height) 365 } 366 367 private fun TouchForwardingLayout.initTouchForwarding(targetView: View) { 368 // Make sure the touch forwarding layout same size of the target view 369 layoutParams = FrameLayout.LayoutParams(targetView.width, targetView.height, Gravity.CENTER) 370 setForwardingEnabled(true) 371 setTargetView(targetView) 372 } 373 374 private fun TouchForwardingLayout.removeTouchForwarding() { 375 setForwardingEnabled(false) 376 setTargetView(null) 377 } 378 } 379