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