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