• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.WallpaperManager
21 import android.content.ContentResolver
22 import android.content.ContentValues
23 import android.content.Context
24 import android.database.ContentObserver
25 import android.graphics.Bitmap
26 import android.graphics.BitmapFactory
27 import android.graphics.Color
28 import android.net.Uri
29 import android.os.Looper
30 import android.util.Log
31 import com.android.wallpaper.model.WallpaperInfo
32 import com.android.wallpaper.module.CurrentWallpaperInfoFactory
33 import com.android.wallpaper.picker.customization.shared.model.WallpaperDestination
34 import com.android.wallpaper.picker.customization.shared.model.WallpaperModel
35 import java.io.IOException
36 import java.util.EnumMap
37 import kotlinx.coroutines.channels.awaitClose
38 import kotlinx.coroutines.flow.Flow
39 import kotlinx.coroutines.flow.callbackFlow
40 import kotlinx.coroutines.launch
41 import kotlinx.coroutines.suspendCancellableCoroutine
42 
43 class WallpaperClientImpl(
44     private val context: Context,
45     private val infoFactory: CurrentWallpaperInfoFactory,
46     private val wallpaperManager: WallpaperManager,
47 ) : WallpaperClient {
48 
49     private var recentsContentProviderAvailable: Boolean? = null
50     private val cachedRecents: MutableMap<WallpaperDestination, List<WallpaperModel>> =
51         EnumMap(WallpaperDestination::class.java)
52 
53     init {
54         if (areRecentsAvailable()) {
55             context.contentResolver.registerContentObserver(
56                 LIST_RECENTS_URI,
57                 /* notifyForDescendants= */ true,
58                 object : ContentObserver(null) {
59                     override fun onChange(selfChange: Boolean) {
60                         cachedRecents.clear()
61                     }
62                 },
63             )
64         }
65     }
66 
67     override fun recentWallpapers(
68         destination: WallpaperDestination,
69         limit: Int,
70     ): Flow<List<WallpaperModel>> {
71         return callbackFlow {
72             // TODO(b/280891780) Remove this check
73             if (Looper.myLooper() == Looper.getMainLooper()) {
74                 throw IllegalStateException("Do not call method recentWallpapers() on main thread")
75             }
76             suspend fun queryAndSend(limit: Int) {
77                 send(queryRecentWallpapers(destination = destination, limit = limit))
78             }
79 
80             val contentObserver =
81                 if (areRecentsAvailable()) {
82                         object : ContentObserver(null) {
83                             override fun onChange(selfChange: Boolean) {
84                                 launch { queryAndSend(limit = limit) }
85                             }
86                         }
87                     } else {
88                         null
89                     }
90                     ?.also {
91                         context.contentResolver.registerContentObserver(
92                             LIST_RECENTS_URI,
93                             /* notifyForDescendants= */ true,
94                             it,
95                         )
96                     }
97             queryAndSend(limit = limit)
98 
99             awaitClose {
100                 if (contentObserver != null) {
101                     context.contentResolver.unregisterContentObserver(contentObserver)
102                 }
103             }
104         }
105     }
106 
107     override suspend fun setWallpaper(
108         destination: WallpaperDestination,
109         wallpaperId: String,
110         onDone: () -> Unit
111     ) {
112         val updateValues = ContentValues()
113         updateValues.put(KEY_ID, wallpaperId)
114         updateValues.put(KEY_SCREEN, destination.asString())
115         val updatedRowCount = context.contentResolver.update(SET_WALLPAPER_URI, updateValues, null)
116         if (updatedRowCount == 0) {
117             Log.e(TAG, "Error setting wallpaper: $wallpaperId")
118         }
119         onDone.invoke()
120     }
121 
122     private suspend fun queryRecentWallpapers(
123         destination: WallpaperDestination,
124         limit: Int,
125     ): List<WallpaperModel> {
126         val recentWallpapers =
127             cachedRecents[destination]
128                 ?: if (!areRecentsAvailable()) {
129                     listOf(getCurrentWallpaperFromFactory(destination))
130                 } else {
131                     queryAllRecentWallpapers(destination)
132                 }
133 
134         cachedRecents[destination] = recentWallpapers
135         return recentWallpapers.take(limit)
136     }
137 
138     private suspend fun queryAllRecentWallpapers(
139         destination: WallpaperDestination
140     ): List<WallpaperModel> {
141         context.contentResolver
142             .query(
143                 LIST_RECENTS_URI.buildUpon().appendPath(destination.asString()).build(),
144                 arrayOf(KEY_ID, KEY_PLACEHOLDER_COLOR, KEY_LAST_UPDATED),
145                 null,
146                 null,
147             )
148             .use { cursor ->
149                 if (cursor == null || cursor.count == 0) {
150                     return emptyList()
151                 }
152 
153                 return buildList {
154                     val idColumnIndex = cursor.getColumnIndex(KEY_ID)
155                     val placeholderColorColumnIndex = cursor.getColumnIndex(KEY_PLACEHOLDER_COLOR)
156                     val lastUpdatedColumnIndex = cursor.getColumnIndex(KEY_LAST_UPDATED)
157                     while (cursor.moveToNext()) {
158                         val wallpaperId = cursor.getString(idColumnIndex)
159                         val placeholderColor = cursor.getInt(placeholderColorColumnIndex)
160                         val lastUpdated = cursor.getLong(lastUpdatedColumnIndex)
161                         add(
162                             WallpaperModel(
163                                 wallpaperId = wallpaperId,
164                                 placeholderColor = placeholderColor,
165                                 lastUpdated = lastUpdated
166                             )
167                         )
168                     }
169                 }
170             }
171     }
172 
173     private suspend fun getCurrentWallpaperFromFactory(
174         destination: WallpaperDestination
175     ): WallpaperModel {
176         val currentWallpapers = getCurrentWallpapers()
177         val wallpaper: WallpaperInfo =
178             if (destination == WallpaperDestination.LOCK) {
179                 currentWallpapers.second ?: currentWallpapers.first
180             } else {
181                 currentWallpapers.first
182             }
183         val colors = wallpaperManager.getWallpaperColors(destination.toFlags())
184 
185         return WallpaperModel(
186             wallpaper.wallpaperId,
187             colors?.primaryColor?.toArgb() ?: Color.TRANSPARENT
188         )
189     }
190 
191     private suspend fun getCurrentWallpapers(): Pair<WallpaperInfo, WallpaperInfo?> =
192         suspendCancellableCoroutine { continuation ->
193             infoFactory.createCurrentWallpaperInfos(
194                 { homeWallpaper, lockWallpaper, _ ->
195                     continuation.resume(Pair(homeWallpaper, lockWallpaper), null)
196                 },
197                 false
198             )
199         }
200 
201     override suspend fun loadThumbnail(
202         wallpaperId: String,
203     ): Bitmap? {
204         if (areRecentsAvailable()) {
205             try {
206                 // We're already using this in a suspend function, so we're okay.
207                 @Suppress("BlockingMethodInNonBlockingContext")
208                 context.contentResolver
209                     .openFile(
210                         GET_THUMBNAIL_BASE_URI.buildUpon().appendPath(wallpaperId).build(),
211                         "r",
212                         null,
213                     )
214                     .use { file ->
215                         if (file == null) {
216                             Log.e(TAG, "Error getting wallpaper preview: $wallpaperId")
217                         } else {
218                             return BitmapFactory.decodeFileDescriptor(file.fileDescriptor)
219                         }
220                     }
221             } catch (e: IOException) {
222                 Log.e(TAG, "Error getting wallpaper preview: $wallpaperId", e)
223             }
224         } else {
225             val currentWallpapers = getCurrentWallpapers()
226             val wallpaper =
227                 if (currentWallpapers.first.wallpaperId == wallpaperId) {
228                     currentWallpapers.first
229                 } else if (currentWallpapers.second?.wallpaperId == wallpaperId) {
230                     currentWallpapers.second
231                 } else null
232             return wallpaper?.getThumbAsset(context)?.getLowResBitmap(context)
233         }
234 
235         return null
236     }
237 
238     override fun areRecentsAvailable(): Boolean {
239         if (recentsContentProviderAvailable == null) {
240             recentsContentProviderAvailable =
241                 try {
242                     context.packageManager.resolveContentProvider(
243                         AUTHORITY,
244                         0,
245                     ) != null
246                 } catch (e: Exception) {
247                     Log.w(
248                         TAG,
249                         "Exception trying to resolve recents content provider, skipping it",
250                         e
251                     )
252                     false
253                 }
254         }
255         return recentsContentProviderAvailable == true
256     }
257 
258     private fun WallpaperDestination.asString(): String {
259         return when (this) {
260             WallpaperDestination.BOTH -> SCREEN_ALL
261             WallpaperDestination.HOME -> SCREEN_HOME
262             WallpaperDestination.LOCK -> SCREEN_LOCK
263         }
264     }
265 
266     private fun WallpaperDestination.toFlags(): Int {
267         return when (this) {
268             WallpaperDestination.BOTH -> WallpaperManager.FLAG_LOCK or WallpaperManager.FLAG_SYSTEM
269             WallpaperDestination.HOME -> WallpaperManager.FLAG_SYSTEM
270             WallpaperDestination.LOCK -> WallpaperManager.FLAG_LOCK
271         }
272     }
273 
274     companion object {
275         private const val TAG = "WallpaperClientImpl"
276         private const val AUTHORITY = "com.google.android.apps.wallpaper.recents"
277 
278         /** Path for making a content provider request to set the wallpaper. */
279         private const val PATH_SET_WALLPAPER = "set_recent_wallpaper"
280         /** Path for making a content provider request to query for the recent wallpapers. */
281         private const val PATH_LIST_RECENTS = "list_recent"
282         /** Path for making a content provider request to query for the thumbnail of a wallpaper. */
283         private const val PATH_GET_THUMBNAIL = "thumb"
284 
285         private val BASE_URI =
286             Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build()
287         /** [Uri] for making a content provider request to set the wallpaper. */
288         private val SET_WALLPAPER_URI = BASE_URI.buildUpon().appendPath(PATH_SET_WALLPAPER).build()
289         /** [Uri] for making a content provider request to query for the recent wallpapers. */
290         private val LIST_RECENTS_URI = BASE_URI.buildUpon().appendPath(PATH_LIST_RECENTS).build()
291         /**
292          * [Uri] for making a content provider request to query for the thumbnail of a wallpaper.
293          */
294         private val GET_THUMBNAIL_BASE_URI =
295             BASE_URI.buildUpon().appendPath(PATH_GET_THUMBNAIL).build()
296 
297         /** Key for a parameter used to pass the wallpaper ID to/from the content provider. */
298         private const val KEY_ID = "id"
299         /** Key for a parameter used to pass the screen to/from the content provider. */
300         private const val KEY_SCREEN = "screen"
301         private const val KEY_LAST_UPDATED = "last_updated"
302         private const val SCREEN_ALL = "all_screens"
303         private const val SCREEN_HOME = "home_screen"
304         private const val SCREEN_LOCK = "lock_screen"
305         /**
306          * Key for a parameter used to get the placeholder color for a wallpaper from the content
307          * provider.
308          */
309         private const val KEY_PLACEHOLDER_COLOR = "placeholder_color"
310     }
311 }
312