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  */
17 
18 package com.android.wallpaper.picker.customization.data.content
19 
20 import android.app.Flags.liveWallpaperContentHandling
21 import android.app.WallpaperColors
22 import android.app.WallpaperManager
23 import android.app.WallpaperManager.FLAG_LOCK
24 import android.app.WallpaperManager.FLAG_SYSTEM
25 import android.app.WallpaperManager.SetWallpaperFlags
26 import android.app.wallpaper.WallpaperDescription
27 import android.content.ComponentName
28 import android.content.ContentResolver
29 import android.content.ContentValues
30 import android.content.Context
31 import android.database.ContentObserver
32 import android.graphics.Bitmap
33 import android.graphics.BitmapFactory
34 import android.graphics.Color
35 import android.graphics.Point
36 import android.graphics.Rect
37 import android.net.Uri
38 import android.util.Log
39 import androidx.exifinterface.media.ExifInterface
40 import com.android.app.tracing.TraceUtils.traceAsync
41 import com.android.wallpaper.asset.Asset
42 import com.android.wallpaper.asset.BitmapUtils
43 import com.android.wallpaper.asset.CurrentWallpaperAsset
44 import com.android.wallpaper.asset.StreamableAsset
45 import com.android.wallpaper.model.LiveWallpaperPrefMetadata
46 import com.android.wallpaper.model.Screen
47 import com.android.wallpaper.model.StaticWallpaperPrefMetadata
48 import com.android.wallpaper.model.WallpaperInfo
49 import com.android.wallpaper.model.WallpaperModelsPair
50 import com.android.wallpaper.module.InjectorProvider
51 import com.android.wallpaper.module.WallpaperPreferences
52 import com.android.wallpaper.module.logging.UserEventLogger
53 import com.android.wallpaper.module.logging.UserEventLogger.SetWallpaperEntryPoint
54 import com.android.wallpaper.picker.customization.shared.model.WallpaperDestination
55 import com.android.wallpaper.picker.customization.shared.model.WallpaperDestination.BOTH
56 import com.android.wallpaper.picker.customization.shared.model.WallpaperDestination.Companion.toDestinationInt
57 import com.android.wallpaper.picker.customization.shared.model.WallpaperDestination.Companion.toSetWallpaperFlags
58 import com.android.wallpaper.picker.customization.shared.model.WallpaperDestination.HOME
59 import com.android.wallpaper.picker.customization.shared.model.WallpaperDestination.LOCK
60 import com.android.wallpaper.picker.customization.shared.model.WallpaperModel as RecentWallpaperModel
61 import com.android.wallpaper.picker.data.WallpaperModel.LiveWallpaperModel
62 import com.android.wallpaper.picker.data.WallpaperModel.StaticWallpaperModel
63 import com.android.wallpaper.picker.di.modules.BackgroundDispatcher
64 import com.android.wallpaper.picker.preview.shared.model.FullPreviewCropModel
65 import com.android.wallpaper.util.WallpaperCropUtils
66 import com.android.wallpaper.util.converter.WallpaperModelFactory
67 import dagger.hilt.android.qualifiers.ApplicationContext
68 import java.io.IOException
69 import java.io.InputStream
70 import javax.inject.Inject
71 import javax.inject.Singleton
72 import kotlinx.coroutines.CancellableContinuation
73 import kotlinx.coroutines.CoroutineScope
74 import kotlinx.coroutines.ExperimentalCoroutinesApi
75 import kotlinx.coroutines.flow.MutableStateFlow
76 import kotlinx.coroutines.flow.asStateFlow
77 import kotlinx.coroutines.flow.filterNotNull
78 import kotlinx.coroutines.flow.take
79 import kotlinx.coroutines.launch
80 import kotlinx.coroutines.suspendCancellableCoroutine
81 
82 @OptIn(ExperimentalCoroutinesApi::class)
83 @Singleton
84 class WallpaperClientImpl
85 @Inject
86 constructor(
87     @ApplicationContext private val context: Context,
88     private val wallpaperManager: WallpaperManager,
89     private val wallpaperPreferences: WallpaperPreferences,
90     private val wallpaperModelFactory: WallpaperModelFactory,
91     private val logger: UserEventLogger,
92     @BackgroundDispatcher val backgroundScope: CoroutineScope,
93 ) : WallpaperClient {
94 
95     private var recentsContentProviderAvailable: Boolean? = null
96     private val recentHomeWallpapers = MutableStateFlow<List<RecentWallpaperModel>?>(null)
97     private val recentLockWallpapers = MutableStateFlow<List<RecentWallpaperModel>?>(null)
98 
99     init {
100         backgroundScope.launch {
101             recentHomeWallpapers.value = queryRecentWallpapers(destination = HOME)
102             recentLockWallpapers.value = queryRecentWallpapers(destination = LOCK)
103         }
104 
105         if (areRecentsAvailable()) {
106             context.contentResolver.registerContentObserver(
107                 LIST_RECENTS_URI,
108                 /* notifyForDescendants= */ true,
109                 object : ContentObserver(null) {
110                     override fun onChange(selfChange: Boolean) {
111                         backgroundScope.launch {
112                             recentHomeWallpapers.value = queryRecentWallpapers(destination = HOME)
113                             recentLockWallpapers.value = queryRecentWallpapers(destination = LOCK)
114                         }
115                     }
116                 },
117             )
118         }
119     }
120 
121     override fun recentWallpapers(destination: WallpaperDestination, limit: Int) =
122         when (destination) {
123             HOME -> recentHomeWallpapers.asStateFlow().filterNotNull().take(limit)
124             LOCK -> recentLockWallpapers.asStateFlow().filterNotNull().take(limit)
125             BOTH ->
126                 throw IllegalStateException(
127                     "Destination $destination should not be used for getting recent wallpapers."
128                 )
129         }
130 
131     override suspend fun setStaticWallpaper(
132         @SetWallpaperEntryPoint setWallpaperEntryPoint: Int,
133         destination: WallpaperDestination,
134         wallpaperModel: StaticWallpaperModel,
135         bitmap: Bitmap,
136         wallpaperSize: Point,
137         asset: Asset,
138         fullPreviewCropModels: Map<Point, FullPreviewCropModel>?,
139     ) {
140         if (destination == HOME || destination == BOTH) {
141             // Disable rotation wallpaper when setting to home screen. Daily rotation rotates
142             // both home and lock screen wallpaper when lock screen is not set; otherwise daily
143             // rotation only rotates home screen while lock screen wallpaper stays as what it's
144             // set to.
145             stopWallpaperRotation()
146         }
147 
148         traceAsync(TAG, "setStaticWallpaper") {
149             val cropHintsWithParallax =
150                 fullPreviewCropModels?.let { cropModels ->
151                     cropModels.mapValues { it.value.adjustCropForParallax(wallpaperSize) }
152                 } ?: emptyMap()
153             val managerId =
154                 wallpaperManager.setStaticWallpaperToSystem(
155                     asset.getStreamOrFromBitmap(bitmap),
156                     bitmap,
157                     cropHintsWithParallax,
158                     destination,
159                     asset,
160                 )
161 
162             wallpaperPreferences.setStaticWallpaperMetadata(
163                 metadata = wallpaperModel.getMetadata(bitmap, managerId),
164                 destination = destination,
165             )
166 
167             logger.logWallpaperApplied(
168                 collectionId = wallpaperModel.commonWallpaperData.id.collectionId,
169                 wallpaperId = wallpaperModel.commonWallpaperData.id.wallpaperId,
170                 effects = null,
171                 setWallpaperEntryPoint = setWallpaperEntryPoint,
172                 destination =
173                     UserEventLogger.toWallpaperDestinationForLogging(destination.toDestinationInt()),
174             )
175 
176             // Save the static wallpaper to recent wallpapers
177             // TODO(b/309138446): check if we can update recent with all cropHints from WM later
178             wallpaperPreferences.addStaticWallpaperToRecentWallpapers(
179                 destination,
180                 wallpaperModel,
181                 bitmap,
182                 cropHintsWithParallax,
183             )
184         }
185     }
186 
187     private fun stopWallpaperRotation() {
188         wallpaperPreferences.setWallpaperPresentationMode(
189             WallpaperPreferences.PRESENTATION_MODE_STATIC
190         )
191         wallpaperPreferences.clearDailyRotations()
192     }
193 
194     /**
195      * Use [WallpaperManager] to set a static wallpaper to the system.
196      *
197      * @return Wallpaper manager ID
198      */
199     private fun WallpaperManager.setStaticWallpaperToSystem(
200         inputStream: InputStream?,
201         bitmap: Bitmap,
202         cropHints: Map<Point, Rect>,
203         destination: WallpaperDestination,
204         asset: Asset,
205     ): Int {
206         // The InputStream of current wallpaper points to system wallpaper file which will be
207         // overwritten during set wallpaper and reads 0 bytes, use Bitmap instead.
208         return if (inputStream != null && asset !is CurrentWallpaperAsset) {
209             setStreamWithCrops(
210                 inputStream,
211                 cropHints,
212                 /* allowBackup= */ true,
213                 destination.toSetWallpaperFlags(),
214             )
215         } else {
216             setBitmapWithCrops(
217                 bitmap,
218                 cropHints,
219                 /* allowBackup= */ true,
220                 destination.toSetWallpaperFlags(),
221             )
222         }
223     }
224 
225     private fun StaticWallpaperModel.getMetadata(
226         bitmap: Bitmap,
227         managerId: Int,
228     ): StaticWallpaperPrefMetadata {
229         val bitmapHash = BitmapUtils.generateHashCode(bitmap)
230         return StaticWallpaperPrefMetadata(
231             commonWallpaperData.attributions,
232             commonWallpaperData.exploreActionUrl,
233             commonWallpaperData.id.collectionId,
234             bitmapHash,
235             managerId,
236             commonWallpaperData.id.uniqueId,
237         )
238     }
239 
240     /**
241      * Save wallpaper metadata in the preference for two purposes:
242      * 1. Quickly reconstruct the currently-selected wallpaper when opening the app
243      * 2. Snapshot logging
244      */
245     private fun WallpaperPreferences.setStaticWallpaperMetadata(
246         metadata: StaticWallpaperPrefMetadata,
247         destination: WallpaperDestination,
248     ) {
249         when (destination) {
250             HOME -> {
251                 clearHomeWallpaperMetadata()
252                 setHomeStaticImageWallpaperMetadata(metadata)
253             }
254             LOCK -> {
255                 clearLockWallpaperMetadata()
256                 setLockStaticImageWallpaperMetadata(metadata)
257             }
258             BOTH -> {
259                 clearHomeWallpaperMetadata()
260                 setHomeStaticImageWallpaperMetadata(metadata)
261                 clearLockWallpaperMetadata()
262                 setLockStaticImageWallpaperMetadata(metadata)
263             }
264         }
265     }
266 
267     override suspend fun setLiveWallpaper(
268         setWallpaperEntryPoint: Int,
269         destination: WallpaperDestination,
270         wallpaperModel: LiveWallpaperModel,
271     ) {
272         if (destination == HOME || destination == BOTH) {
273             // Disable rotation wallpaper when setting to home screen. Daily rotation rotates
274             // both home and lock screen wallpaper when lock screen is not set; otherwise daily
275             // rotation only rotates home screen while lock screen wallpaper stays as what it's
276             // set to.
277             stopWallpaperRotation()
278         }
279 
280         traceAsync(TAG, "setLiveWallpaper") {
281             val managerId = wallpaperManager.setLiveWallpaperToSystem(wallpaperModel, destination)
282 
283             wallpaperPreferences.setLiveWallpaperMetadata(
284                 metadata = wallpaperModel.getMetadata(managerId),
285                 destination = destination,
286             )
287 
288             logger.logWallpaperApplied(
289                 collectionId = wallpaperModel.commonWallpaperData.id.collectionId,
290                 wallpaperId = wallpaperModel.commonWallpaperData.id.wallpaperId,
291                 effects = wallpaperModel.liveWallpaperData.effectNames,
292                 setWallpaperEntryPoint = setWallpaperEntryPoint,
293                 destination =
294                     UserEventLogger.toWallpaperDestinationForLogging(destination.toDestinationInt()),
295             )
296 
297             wallpaperPreferences.addLiveWallpaperToRecentWallpapers(destination, wallpaperModel)
298         }
299     }
300 
301     private fun tryAndroidBSetComponent(
302         wallpaperModel: LiveWallpaperModel,
303         destination: WallpaperDestination,
304     ): Boolean {
305         try {
306             val method =
307                 wallpaperManager.javaClass.getMethod(
308                     "setWallpaperComponentWithDescription",
309                     WallpaperDescription::class.java,
310                     Int::class.javaPrimitiveType,
311                 )
312             method.invoke(
313                 wallpaperManager,
314                 wallpaperModel.liveWallpaperData.description,
315                 destination.toSetWallpaperFlags(),
316             )
317             return true
318         } catch (e: NoSuchMethodException) {
319             return false
320         }
321     }
322 
323     private fun tryAndroidUSetComponent(
324         wallpaperModel: LiveWallpaperModel,
325         destination: WallpaperDestination,
326     ): Boolean {
327         try {
328             val method =
329                 wallpaperManager.javaClass.getMethod(
330                     "setWallpaperComponentWithFlags",
331                     ComponentName::class.java,
332                     Int::class.javaPrimitiveType,
333                 )
334             method.invoke(
335                 wallpaperManager,
336                 wallpaperModel.commonWallpaperData.id.componentName,
337                 destination.toSetWallpaperFlags(),
338             )
339             if (liveWallpaperContentHandling()) {
340                 Log.w(
341                     TAG,
342                     "live wallpaper content handling enabled, but Android U setWallpaperComponentWithFlags called",
343                 )
344             }
345             return true
346         } catch (e: NoSuchMethodException) {
347             return false
348         }
349     }
350 
351     /**
352      * Use [WallpaperManager] to set a live wallpaper to the system.
353      *
354      * @return Wallpaper manager ID
355      */
356     private fun WallpaperManager.setLiveWallpaperToSystem(
357         wallpaperModel: LiveWallpaperModel,
358         destination: WallpaperDestination,
359     ): Int {
360         if (tryAndroidBSetComponent(wallpaperModel, destination)) {
361             // intentional no-op
362         } else if (tryAndroidUSetComponent(wallpaperModel, destination)) {
363             // intentional no-op
364         } else {
365             setWallpaperComponent(wallpaperModel.commonWallpaperData.id.componentName)
366         }
367 
368         // Be careful that WallpaperManager.getWallpaperId can only accept either
369         // WallpaperManager.FLAG_SYSTEM or WallpaperManager.FLAG_LOCK.
370         // If destination is BOTH, either flag should return the same wallpaper manager ID.
371         return getWallpaperId(
372             if (destination == BOTH || destination == HOME) FLAG_SYSTEM else FLAG_LOCK
373         )
374     }
375 
376     private fun LiveWallpaperModel.getMetadata(managerId: Int): LiveWallpaperPrefMetadata {
377         return LiveWallpaperPrefMetadata(
378             commonWallpaperData.attributions,
379             liveWallpaperData.systemWallpaperInfo.serviceName,
380             liveWallpaperData.effectNames,
381             commonWallpaperData.id.collectionId,
382             managerId,
383         )
384     }
385 
386     /**
387      * Save wallpaper metadata in the preference for two purposes:
388      * 1. Quickly reconstruct the currently-selected wallpaper when opening the app
389      * 2. Snapshot logging
390      */
391     private fun WallpaperPreferences.setLiveWallpaperMetadata(
392         metadata: LiveWallpaperPrefMetadata,
393         destination: WallpaperDestination,
394     ) {
395         when (destination) {
396             HOME -> {
397                 clearHomeWallpaperMetadata()
398                 setHomeLiveWallpaperMetadata(metadata)
399             }
400             LOCK -> {
401                 clearLockWallpaperMetadata()
402                 setLockLiveWallpaperMetadata(metadata)
403             }
404             BOTH -> {
405                 clearHomeWallpaperMetadata()
406                 setHomeLiveWallpaperMetadata(metadata)
407                 clearLockWallpaperMetadata()
408                 setLockLiveWallpaperMetadata(metadata)
409             }
410         }
411     }
412 
413     override suspend fun setRecentWallpaper(
414         @SetWallpaperEntryPoint setWallpaperEntryPoint: Int,
415         destination: WallpaperDestination,
416         wallpaperId: String,
417         onDone: () -> Unit,
418     ) {
419         val updateValues = ContentValues()
420         updateValues.put(KEY_ID, wallpaperId)
421         updateValues.put(KEY_SCREEN, destination.asString())
422         updateValues.put(KEY_SET_WALLPAPER_ENTRY_POINT, setWallpaperEntryPoint)
423         traceAsync(TAG, "setRecentWallpaper") {
424             val updatedRowCount =
425                 context.contentResolver.update(SET_WALLPAPER_URI, updateValues, null)
426             if (updatedRowCount == 0) {
427                 Log.e(TAG, "Error setting wallpaper: $wallpaperId")
428             }
429             onDone.invoke()
430         }
431     }
432 
433     private suspend fun queryRecentWallpapers(
434         destination: WallpaperDestination
435     ): List<RecentWallpaperModel> =
436         if (!areRecentsAvailable()) {
437             listOf(getCurrentWallpaperFromFactory(destination))
438         } else {
439             queryAllRecentWallpapers(destination)
440         }
441 
442     private fun queryAllRecentWallpapers(
443         destination: WallpaperDestination
444     ): List<RecentWallpaperModel> {
445         context.contentResolver
446             .query(
447                 LIST_RECENTS_URI.buildUpon().appendPath(destination.asString()).build(),
448                 arrayOf(KEY_ID, KEY_PLACEHOLDER_COLOR, KEY_LAST_UPDATED),
449                 null,
450                 null,
451             )
452             .use { cursor ->
453                 if (cursor == null || cursor.count == 0) {
454                     return emptyList()
455                 }
456 
457                 return buildList {
458                     val idColumnIndex = cursor.getColumnIndex(KEY_ID)
459                     val placeholderColorColumnIndex = cursor.getColumnIndex(KEY_PLACEHOLDER_COLOR)
460                     val lastUpdatedColumnIndex = cursor.getColumnIndex(KEY_LAST_UPDATED)
461                     val titleColumnIndex = cursor.getColumnIndex(TITLE)
462                     while (cursor.moveToNext()) {
463                         val wallpaperId = cursor.getString(idColumnIndex)
464                         val placeholderColor = cursor.getInt(placeholderColorColumnIndex)
465                         val lastUpdated = cursor.getLong(lastUpdatedColumnIndex)
466                         val title =
467                             if (titleColumnIndex > -1) cursor.getString(titleColumnIndex) else null
468 
469                         add(
470                             RecentWallpaperModel(
471                                 wallpaperId = wallpaperId,
472                                 placeholderColor = placeholderColor,
473                                 lastUpdated = lastUpdated,
474                                 title = title,
475                             )
476                         )
477                     }
478                 }
479             }
480     }
481 
482     private suspend fun getCurrentWallpaperFromFactory(
483         destination: WallpaperDestination
484     ): RecentWallpaperModel {
485         val currentWallpapers = getCurrentWallpapers()
486         val wallpaper: WallpaperInfo =
487             if (destination == LOCK) {
488                 currentWallpapers.second ?: currentWallpapers.first
489             } else {
490                 currentWallpapers.first
491             }
492         val colors = wallpaperManager.getWallpaperColors(destination.toSetWallpaperFlags())
493 
494         return RecentWallpaperModel(
495             wallpaperId = wallpaper.wallpaperId,
496             placeholderColor = colors?.primaryColor?.toArgb() ?: Color.TRANSPARENT,
497             title = wallpaper.getTitle(context),
498         )
499     }
500 
501     private suspend fun getCurrentWallpapers(): Pair<WallpaperInfo, WallpaperInfo?> =
502         suspendCancellableCoroutine { continuation ->
503             InjectorProvider.getInjector()
504                 .getCurrentWallpaperInfoFactory(context)
505                 .createCurrentWallpaperInfos(context, /* forceRefresh= */ false) {
506                     homeWallpaper,
507                     lockWallpaper,
508                     _ ->
509                     continuation.resume(Pair(homeWallpaper, lockWallpaper), null)
510                 }
511         }
512 
513     override suspend fun getCurrentWallpaperModels(): WallpaperModelsPair {
514         val currentWallpapers = getCurrentWallpapers()
515         val homeWallpaper = currentWallpapers.first
516         val lockWallpaper = currentWallpapers.second
517         return WallpaperModelsPair(
518             wallpaperModelFactory.getWallpaperModel(context, homeWallpaper),
519             lockWallpaper?.let { wallpaperModelFactory.getWallpaperModel(context, it) },
520         )
521     }
522 
523     override suspend fun loadThumbnail(
524         wallpaperId: String,
525         destination: WallpaperDestination,
526     ): Bitmap? {
527         if (areRecentsAvailable()) {
528             try {
529                 // We're already using this in a suspend function, so we're okay.
530                 @Suppress("BlockingMethodInNonBlockingContext")
531                 context.contentResolver
532                     .openFile(
533                         GET_THUMBNAIL_BASE_URI.buildUpon()
534                             .appendPath(wallpaperId)
535                             .appendQueryParameter(KEY_DESTINATION, destination.asString())
536                             .build(),
537                         "r",
538                         null,
539                     )
540                     .use { file ->
541                         if (file == null) {
542                             Log.e(TAG, "Error getting wallpaper preview: $wallpaperId")
543                         } else {
544                             return BitmapFactory.decodeFileDescriptor(file.fileDescriptor)
545                         }
546                     }
547             } catch (e: IOException) {
548                 Log.e(
549                     TAG,
550                     "Error getting wallpaper preview: $wallpaperId, destination: ${destination.asString()}",
551                     e,
552                 )
553             }
554         } else {
555             val currentWallpapers = getCurrentWallpapers()
556             val wallpaper =
557                 if (currentWallpapers.first.wallpaperId == wallpaperId) {
558                     currentWallpapers.first
559                 } else if (currentWallpapers.second?.wallpaperId == wallpaperId) {
560                     currentWallpapers.second
561                 } else null
562             return wallpaper?.getThumbAsset(context)?.getLowResBitmap(context)
563         }
564 
565         return null
566     }
567 
568     override fun areRecentsAvailable(): Boolean {
569         if (recentsContentProviderAvailable == null) {
570             recentsContentProviderAvailable =
571                 try {
572                     context.packageManager.resolveContentProvider(AUTHORITY, 0) != null
573                 } catch (e: Exception) {
574                     Log.w(
575                         TAG,
576                         "Exception trying to resolve recents content provider, skipping it",
577                         e,
578                     )
579                     false
580                 }
581         }
582         return recentsContentProviderAvailable == true
583     }
584 
585     override fun getCurrentCropHints(
586         displaySizes: List<Point>,
587         @SetWallpaperFlags which: Int,
588     ): Map<Point, Rect>? {
589         val flags = InjectorProvider.getInjector().getFlags()
590         if (!flags.isMultiCropEnabled()) {
591             return null
592         }
593         val cropHints: List<Rect>? =
594             wallpaperManager.getBitmapCrops(displaySizes, which, /* originalBitmap= */ true)
595 
596         return cropHints?.indices?.associate { displaySizes[it] to cropHints[it] }
597     }
598 
599     override suspend fun getWallpaperColors(
600         bitmap: Bitmap,
601         cropHints: Map<Point, Rect>?,
602     ): WallpaperColors? {
603         return wallpaperManager.getWallpaperColors(bitmap, cropHints)
604     }
605 
606     override fun getWallpaperColors(screen: Screen): WallpaperColors? {
607         return wallpaperManager.getWallpaperColors(
608             if (screen == Screen.LOCK_SCREEN) {
609                 FLAG_LOCK
610             } else {
611                 FLAG_SYSTEM
612             }
613         )
614     }
615 
616     fun WallpaperDestination.asString(): String {
617         return when (this) {
618             BOTH -> SCREEN_ALL
619             HOME -> SCREEN_HOME
620             LOCK -> SCREEN_LOCK
621         }
622     }
623 
624     /**
625      * Adjusts cropHints for parallax effect.
626      *
627      * [WallpaperCropUtils.calculateCropRect] calculates based on the scaled size, the scale depends
628      * on the view size hosting the preview and the wallpaper zoom of the preview on that view,
629      * whereas the rest of multi-crop is based on full wallpaper size. So scaled back at the end.
630      *
631      * If [CropSizeModel] is null, returns the original cropHint without parallax.
632      *
633      * @param wallpaperSize full wallpaper image size.
634      */
635     private fun FullPreviewCropModel.adjustCropForParallax(wallpaperSize: Point): Rect {
636         return cropSizeModel?.let {
637             WallpaperCropUtils.calculateCropRect(
638                     context,
639                     it.hostViewSize,
640                     it.cropViewSize,
641                     wallpaperSize,
642                     cropHint,
643                     it.wallpaperZoom,
644                     /* cropExtraWidth= */ true,
645                 )
646                 .apply {
647                     scale(1f / it.wallpaperZoom)
648                     if (right > wallpaperSize.x) right = wallpaperSize.x
649                     if (bottom > wallpaperSize.y) bottom = wallpaperSize.y
650                 }
651         } ?: cropHint
652     }
653 
654     private suspend fun Asset.getStreamOrFromBitmap(bitmap: Bitmap): InputStream? =
655         suspendCancellableCoroutine { k: CancellableContinuation<InputStream?> ->
656             if (this is StreamableAsset) {
657                 if (exifOrientation != ExifInterface.ORIENTATION_NORMAL) {
658                     k.resumeWith(Result.success(BitmapUtils.bitmapToInputStream(bitmap)))
659                 } else {
660                     fetchInputStream { k.resumeWith(Result.success(it)) }
661                 }
662             } else {
663                 k.resumeWith(Result.success(null))
664             }
665         }
666 
667     companion object {
668         private const val TAG = "WallpaperClientImpl"
669         private const val AUTHORITY = "com.google.android.apps.wallpaper.recents"
670 
671         /** Path for making a content provider request to set the wallpaper. */
672         private const val PATH_SET_WALLPAPER = "set_recent_wallpaper"
673         /** Path for making a content provider request to query for the recent wallpapers. */
674         private const val PATH_LIST_RECENTS = "list_recent"
675         /** Path for making a content provider request to query for the thumbnail of a wallpaper. */
676         private const val PATH_GET_THUMBNAIL = "thumb"
677 
678         private val BASE_URI =
679             Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build()
680         /** [Uri] for making a content provider request to set the wallpaper. */
681         private val SET_WALLPAPER_URI = BASE_URI.buildUpon().appendPath(PATH_SET_WALLPAPER).build()
682         /** [Uri] for making a content provider request to query for the recent wallpapers. */
683         private val LIST_RECENTS_URI = BASE_URI.buildUpon().appendPath(PATH_LIST_RECENTS).build()
684         /**
685          * [Uri] for making a content provider request to query for the thumbnail of a wallpaper.
686          */
687         private val GET_THUMBNAIL_BASE_URI =
688             BASE_URI.buildUpon().appendPath(PATH_GET_THUMBNAIL).build()
689 
690         /** Key for a parameter used to pass the wallpaper ID to/from the content provider. */
691         private const val KEY_ID = "id"
692         /** Key for a parameter used to pass the screen to/from the content provider. */
693         private const val KEY_SCREEN = "screen"
694         /** Key for a parameter used to pass the wallpaper destination to/from content provider. */
695         private const val KEY_DESTINATION = "destination"
696         /** Key for a parameter used to pass the screen to/from the content provider. */
697         private const val KEY_SET_WALLPAPER_ENTRY_POINT = "set_wallpaper_entry_point"
698         private const val KEY_LAST_UPDATED = "last_updated"
699         private const val SCREEN_ALL = "all_screens"
700         private const val SCREEN_HOME = "home_screen"
701         private const val SCREEN_LOCK = "lock_screen"
702 
703         private const val TITLE = "title"
704         /**
705          * Key for a parameter used to get the placeholder color for a wallpaper from the content
706          * provider.
707          */
708         private const val KEY_PLACEHOLDER_COLOR = "placeholder_color"
709     }
710 }
711