1 /* <lambda>null2 * Copyright (C) 2024 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 package com.android.wallpaper.picker.preview.data.repository 18 19 import android.app.WallpaperInfo 20 import android.app.WallpaperManager 21 import android.content.ComponentName 22 import android.content.Context 23 import android.content.Intent 24 import android.content.pm.PackageManager 25 import android.net.Uri 26 import android.os.Bundle 27 import android.os.Handler 28 import android.os.Looper 29 import android.service.wallpaper.WallpaperService 30 import android.stats.style.StyleEnums 31 import android.util.Log 32 import com.android.wallpaper.config.BaseFlags 33 import com.android.wallpaper.effects.Effect 34 import com.android.wallpaper.effects.EffectContract 35 import com.android.wallpaper.effects.EffectsController 36 import com.android.wallpaper.effects.EffectsController.EffectEnumInterface 37 import com.android.wallpaper.module.logging.UserEventLogger 38 import com.android.wallpaper.picker.data.LiveWallpaperData 39 import com.android.wallpaper.picker.data.WallpaperId 40 import com.android.wallpaper.picker.data.WallpaperModel 41 import com.android.wallpaper.picker.data.WallpaperModel.LiveWallpaperModel 42 import com.android.wallpaper.picker.data.WallpaperModel.StaticWallpaperModel 43 import com.android.wallpaper.picker.di.modules.BackgroundDispatcher 44 import com.android.wallpaper.picker.preview.data.repository.ImageEffectsRepository.EffectStatus 45 import com.android.wallpaper.picker.preview.shared.model.ImageEffectsModel 46 import com.android.wallpaper.widget.floatingsheetcontent.WallpaperEffectsView2.EffectTextRes 47 import dagger.hilt.android.qualifiers.ApplicationContext 48 import dagger.hilt.android.scopes.ActivityRetainedScoped 49 import java.io.IOException 50 import javax.inject.Inject 51 import kotlinx.coroutines.CoroutineDispatcher 52 import kotlinx.coroutines.flow.MutableStateFlow 53 import kotlinx.coroutines.flow.asStateFlow 54 import kotlinx.coroutines.withContext 55 import org.xmlpull.v1.XmlPullParserException 56 57 @ActivityRetainedScoped 58 class ImageEffectsRepositoryImpl 59 @Inject 60 constructor( 61 @ApplicationContext private val context: Context, 62 private val effectsController: EffectsController, 63 private val logger: UserEventLogger, 64 @BackgroundDispatcher private val bgDispatcher: CoroutineDispatcher, 65 ) : ImageEffectsRepository { 66 private val _imageEffectsModel = 67 MutableStateFlow(ImageEffectsModel(EffectStatus.EFFECT_DISABLE)) 68 override val imageEffectsModel = _imageEffectsModel.asStateFlow() 69 private val _wallpaperEffect = MutableStateFlow<Effect?>(null) 70 override val wallpaperEffect = _wallpaperEffect.asStateFlow() 71 // This StaticWallpaperModel is set when initializing the repository and used for 72 // 1. Providing essential data to construct LiveWallpaperData when effect is enabled 73 // 2. Reverting back to the original static image wallpaper when effect is disabled 74 private lateinit var staticWallpaperModel: StaticWallpaperModel 75 private lateinit var onWallpaperUpdated: (wallpaper: WallpaperModel) -> Unit 76 77 private val timeOutHandler: Handler = Handler(Looper.getMainLooper()) 78 private var startGeneratingTime = 0L 79 private var startDownloadTime = 0L 80 81 /** Returns whether effects are available at all on the device */ 82 override fun areEffectsAvailable(): Boolean { 83 return effectsController.areEffectsAvailable() 84 } 85 86 override suspend fun initializeEffect( 87 staticWallpaperModel: StaticWallpaperModel, 88 onWallpaperModelUpdated: (wallpaper: WallpaperModel) -> Unit 89 ) { 90 this.staticWallpaperModel = staticWallpaperModel 91 onWallpaperUpdated = onWallpaperModelUpdated 92 93 withContext(bgDispatcher) { 94 val listener = 95 EffectsController.EffectsServiceListener { 96 effect, 97 bundle, 98 resultCode, 99 originalStatusCode, 100 errorMessage -> 101 timeOutHandler.removeCallbacksAndMessages(null) 102 when (resultCode) { 103 EffectsController.RESULT_PROBE_SUCCESS -> { 104 _imageEffectsModel.value = 105 ImageEffectsModel(EffectStatus.EFFECT_READY, resultCode) 106 } 107 EffectsController.RESULT_PROBE_ERROR -> { 108 // Do nothing intended 109 } 110 EffectsController.RESULT_ERROR_PROBE_SUPPORT_FOREGROUND -> { 111 if (BaseFlags.get().isWallpaperEffectModelDownloadEnabled()) { 112 _imageEffectsModel.value = 113 ImageEffectsModel( 114 EffectStatus.EFFECT_DOWNLOAD_READY, 115 resultCode, 116 ) 117 } 118 } 119 EffectsController.RESULT_PROBE_FOREGROUND_DOWNLOADING -> { 120 _imageEffectsModel.value = 121 ImageEffectsModel( 122 EffectStatus.EFFECT_DOWNLOAD_IN_PROGRESS, 123 resultCode, 124 ) 125 } 126 EffectsController.RESULT_FOREGROUND_DOWNLOAD_SUCCEEDED -> { 127 _imageEffectsModel.value = 128 ImageEffectsModel(EffectStatus.EFFECT_READY, resultCode) 129 logger.logEffectForegroundDownload( 130 getEffectNameForLogging(), 131 StyleEnums.EFFECT_APPLIED_ON_SUCCESS, 132 System.currentTimeMillis() - startDownloadTime, 133 ) 134 } 135 EffectsController.RESULT_FOREGROUND_DOWNLOAD_FAILED -> { 136 _imageEffectsModel.value = 137 ImageEffectsModel( 138 EffectStatus.EFFECT_DOWNLOAD_FAILED, 139 resultCode, 140 errorMessage, 141 ) 142 logger.logEffectForegroundDownload( 143 getEffectNameForLogging(), 144 StyleEnums.EFFECT_APPLIED_ON_FAILED, 145 System.currentTimeMillis() - startDownloadTime, 146 ) 147 } 148 EffectsController.RESULT_SUCCESS, 149 EffectsController.RESULT_SUCCESS_WITH_GENERATION_ERROR -> { 150 _imageEffectsModel.value = 151 ImageEffectsModel( 152 EffectStatus.EFFECT_APPLIED, 153 resultCode, 154 errorMessage, 155 ) 156 bundle.getCinematicWallpaperModel(effect)?.let { 157 onWallpaperUpdated.invoke(it) 158 } 159 logger.logEffectApply( 160 getEffectNameForLogging(), 161 StyleEnums.EFFECT_APPLIED_ON_SUCCESS, 162 /* timeElapsedMillis= */ System.currentTimeMillis() - 163 startGeneratingTime, 164 /* resultCode= */ originalStatusCode, 165 ) 166 } 167 EffectsController.RESULT_SUCCESS_REUSED -> { 168 _imageEffectsModel.value = 169 ImageEffectsModel(EffectStatus.EFFECT_APPLIED, resultCode) 170 bundle.getCinematicWallpaperModel(effect)?.let { 171 onWallpaperUpdated.invoke(it) 172 } 173 } 174 EffectsController.RESULT_ERROR_TRY_AGAIN_LATER -> { 175 _imageEffectsModel.value = 176 ImageEffectsModel( 177 EffectStatus.EFFECT_APPLY_FAILED, 178 resultCode, 179 errorMessage, 180 ) 181 logger.logEffectApply( 182 getEffectNameForLogging(), 183 StyleEnums.EFFECT_APPLIED_ON_FAILED, 184 /* timeElapsedMillis= */ System.currentTimeMillis() - 185 startGeneratingTime, 186 /* resultCode= */ originalStatusCode 187 ) 188 } 189 else -> { 190 _imageEffectsModel.value = 191 ImageEffectsModel( 192 EffectStatus.EFFECT_APPLY_FAILED, 193 resultCode, 194 errorMessage, 195 ) 196 logger.logEffectApply( 197 getEffectNameForLogging(), 198 StyleEnums.EFFECT_APPLIED_ON_FAILED, 199 /* timeElapsedMillis= */ System.currentTimeMillis() - 200 startGeneratingTime, 201 /* resultCode= */ originalStatusCode, 202 ) 203 } 204 } 205 } 206 effectsController.setListener(listener) 207 208 effectsController.contentUri.let { uri -> 209 if (Uri.EMPTY.equals(uri)) { 210 return@withContext 211 } 212 213 // Query effect provider 214 context.contentResolver 215 .query( 216 uri, 217 /* projection= */ null, 218 /* selection= */ null, 219 /* selectionArgs= */ null, 220 /* sortOrder= */ null 221 ) 222 ?.use { 223 while (it.moveToNext()) { 224 val titleRow: Int = it.getColumnIndex(EffectContract.KEY_EFFECT_TITLE) 225 val idRow: Int = it.getColumnIndex(EffectContract.KEY_EFFECT_ID) 226 _wallpaperEffect.value = 227 Effect( 228 it.getInt(idRow), 229 it.getString(titleRow), 230 effectsController.targetEffect, 231 ) 232 } 233 } 234 } 235 236 if (effectsController.isEffectTriggered) { 237 _imageEffectsModel.value = ImageEffectsModel(EffectStatus.EFFECT_READY) 238 } else { 239 effectsController.triggerEffect(context) 240 } 241 } 242 } 243 244 private fun Bundle.getCinematicWallpaperModel( 245 effect: EffectEnumInterface 246 ): LiveWallpaperModel? { 247 val componentName = 248 if (containsKey(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT)) { 249 getParcelable<ComponentName>(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT) 250 } else { 251 null 252 } 253 ?: return null 254 255 val assetId = 256 if (containsKey(EffectContract.ASSET_ID)) { 257 getInt(EffectContract.ASSET_ID).toString() 258 } else null 259 260 val resolveInfos = 261 Intent(WallpaperService.SERVICE_INTERFACE) 262 .apply { setClassName(componentName.packageName, componentName.className) } 263 .let { 264 context.packageManager.queryIntentServices(it, PackageManager.GET_META_DATA) 265 } 266 if (resolveInfos.isEmpty()) { 267 Log.w(TAG, "Couldn't find live wallpaper for " + componentName.className) 268 return null 269 } 270 271 try { 272 val wallpaperInfo = WallpaperInfo(context, resolveInfos[0]) 273 val commonWallpaperData = 274 staticWallpaperModel.commonWallpaperData.copy( 275 id = 276 WallpaperId( 277 componentName = componentName, 278 uniqueId = 279 if (assetId != null) "${wallpaperInfo.serviceName}_$assetId" 280 else wallpaperInfo.serviceName, 281 collectionId = staticWallpaperModel.commonWallpaperData.id.collectionId, 282 ), 283 ) 284 val liveWallpaperData = 285 LiveWallpaperData( 286 groupName = "", 287 systemWallpaperInfo = wallpaperInfo, 288 isTitleVisible = false, 289 isApplied = false, 290 isEffectWallpaper = effectsController.isEffectsWallpaper(wallpaperInfo), 291 effectNames = effect.toString(), 292 ) 293 return LiveWallpaperModel( 294 commonWallpaperData = commonWallpaperData, 295 liveWallpaperData = liveWallpaperData, 296 creativeWallpaperData = null, 297 internalLiveWallpaperData = null, 298 ) 299 } catch (e: XmlPullParserException) { 300 Log.w(TAG, "Skipping wallpaper " + resolveInfos[0].serviceInfo, e) 301 return null 302 } catch (e: IOException) { 303 Log.w(TAG, "Skipping wallpaper " + resolveInfos[0].serviceInfo, e) 304 return null 305 } 306 } 307 308 override fun enableImageEffect(effect: EffectEnumInterface) { 309 startGeneratingTime = System.currentTimeMillis() 310 _imageEffectsModel.value = ImageEffectsModel(EffectStatus.EFFECT_APPLY_IN_PROGRESS) 311 val uri = staticWallpaperModel.imageWallpaperData?.uri ?: return 312 effectsController.generateEffect(effect, uri) 313 timeOutHandler.postDelayed( 314 { 315 wallpaperEffect.value?.let { effectsController.interruptGenerate(it) } 316 _imageEffectsModel.value = 317 ImageEffectsModel( 318 EffectStatus.EFFECT_APPLY_FAILED, 319 null, 320 effectsController.retryInstruction, 321 ) 322 logger.logEffectApply( 323 getEffectNameForLogging(), 324 StyleEnums.EFFECT_APPLIED_ON_FAILED, 325 System.currentTimeMillis() - startGeneratingTime, 326 EffectsController.ERROR_ORIGINAL_TIME_OUT, 327 ) 328 }, 329 TIME_OUT_TIME_IN_MS 330 ) 331 } 332 333 override fun disableImageEffect() { 334 _imageEffectsModel.value = ImageEffectsModel(EffectStatus.EFFECT_READY) 335 logger.logEffectApply( 336 wallpaperEffect.value?.type.toString(), 337 StyleEnums.EFFECT_APPLIED_OFF, 338 0L, 339 0, 340 ) 341 onWallpaperUpdated.invoke(staticWallpaperModel) 342 } 343 344 override fun destroy() { 345 timeOutHandler.removeCallbacksAndMessages(null) 346 effectsController.removeListener() 347 // We need to call interruptGenerate() and destroy() to make sure there is no cached effect 348 // wallpaper overriding the currently-selected effect wallpaper preview. 349 wallpaperEffect.value?.let { effectsController.interruptGenerate(it) } 350 effectsController.destroy() 351 _wallpaperEffect.value = null 352 } 353 354 override fun isTargetEffect(effect: EffectEnumInterface): Boolean { 355 return effectsController.isTargetEffect(effect) 356 } 357 358 override fun getEffectTextRes(): EffectTextRes { 359 return EffectTextRes( 360 effectsController.effectTitle, 361 effectsController.effectFailedTitle, 362 effectsController.effectSubTitle, 363 effectsController.retryInstruction, 364 effectsController.noEffectInstruction, 365 ) 366 } 367 368 /** 369 * This function triggers the downloading of the machine learning models. The downloading occurs 370 * in the foreground off the main thread so it's safe to trigger it from the main thread. 371 */ 372 override fun startEffectsModelDownload(effect: Effect) { 373 _imageEffectsModel.value = ImageEffectsModel(EffectStatus.EFFECT_DOWNLOAD_IN_PROGRESS) 374 effectsController.startForegroundDownload(effect) 375 startDownloadTime = System.currentTimeMillis() 376 logger.logEffectForegroundDownload( 377 getEffectNameForLogging(), 378 StyleEnums.EFFECT_APPLIED_STARTED, 379 0, 380 ) 381 } 382 383 override fun interruptEffectsModelDownload(effect: Effect) { 384 _imageEffectsModel.value = ImageEffectsModel(EffectStatus.EFFECT_DOWNLOAD_READY) 385 effectsController.interruptForegroundDownload(effect) 386 } 387 388 private fun getEffectNameForLogging(): String { 389 val effect = wallpaperEffect.value 390 return effect?.type?.toString() ?: EffectsController.Effect.UNKNOWN.toString() 391 } 392 393 companion object { 394 private const val TAG = "EffectsRepository" 395 private const val TIME_OUT_TIME_IN_MS = 90000L 396 } 397 } 398