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