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 package com.android.wallpaper.picker.preview.ui 17 18 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM 19 import android.content.Context 20 import android.content.Intent 21 import android.content.pm.ActivityInfo 22 import android.graphics.Color 23 import android.os.Bundle 24 import android.view.Window 25 import android.widget.Toast 26 import androidx.activity.result.contract.ActivityResultContracts 27 import androidx.activity.viewModels 28 import androidx.core.view.WindowCompat 29 import androidx.lifecycle.lifecycleScope 30 import androidx.navigation.fragment.NavHostFragment 31 import com.android.wallpaper.R 32 import com.android.wallpaper.config.BaseFlags 33 import com.android.wallpaper.model.ImageWallpaperInfo 34 import com.android.wallpaper.model.WallpaperInfo 35 import com.android.wallpaper.module.InjectorProvider 36 import com.android.wallpaper.picker.AppbarFragment 37 import com.android.wallpaper.picker.BasePreviewActivity 38 import com.android.wallpaper.picker.category.ui.viewmodel.CategoriesViewModel 39 import com.android.wallpaper.picker.common.preview.data.repository.PersistentWallpaperModelRepository 40 import com.android.wallpaper.picker.data.WallpaperModel 41 import com.android.wallpaper.picker.di.modules.MainDispatcher 42 import com.android.wallpaper.picker.preview.data.repository.CreativeEffectsRepository 43 import com.android.wallpaper.picker.preview.data.repository.ImageEffectsRepository 44 import com.android.wallpaper.picker.preview.data.repository.WallpaperPreviewRepository 45 import com.android.wallpaper.picker.preview.data.util.LiveWallpaperDownloader 46 import com.android.wallpaper.picker.preview.ui.fragment.SmallPreviewFragment 47 import com.android.wallpaper.picker.preview.ui.viewmodel.PreviewActionsViewModel.Companion.getEditActivityIntent 48 import com.android.wallpaper.picker.preview.ui.viewmodel.PreviewActionsViewModel.Companion.isNewCreativeWallpaper 49 import com.android.wallpaper.picker.preview.ui.viewmodel.WallpaperPreviewViewModel 50 import com.android.wallpaper.util.ActivityUtils 51 import com.android.wallpaper.util.DisplayUtils 52 import com.android.wallpaper.util.WallpaperConnection 53 import com.android.wallpaper.util.converter.WallpaperModelFactory 54 import com.android.wallpaper.util.wallpaperconnection.WallpaperConnectionUtils 55 import dagger.hilt.android.AndroidEntryPoint 56 import dagger.hilt.android.qualifiers.ApplicationContext 57 import javax.inject.Inject 58 import kotlinx.coroutines.CoroutineScope 59 import kotlinx.coroutines.launch 60 61 /** This activity holds the flow for the preview screen. */ 62 @AndroidEntryPoint(BasePreviewActivity::class) 63 class WallpaperPreviewActivity : 64 Hilt_WallpaperPreviewActivity(), AppbarFragment.AppbarFragmentHost { 65 @ApplicationContext @Inject lateinit var appContext: Context 66 @Inject lateinit var displayUtils: DisplayUtils 67 @Inject lateinit var wallpaperModelFactory: WallpaperModelFactory 68 @Inject lateinit var wallpaperPreviewRepository: WallpaperPreviewRepository 69 @Inject lateinit var imageEffectsRepository: ImageEffectsRepository 70 @Inject lateinit var creativeEffectsRepository: CreativeEffectsRepository 71 @Inject lateinit var persistentWallpaperModelRepository: PersistentWallpaperModelRepository 72 @Inject lateinit var liveWallpaperDownloader: LiveWallpaperDownloader 73 @MainDispatcher @Inject lateinit var mainScope: CoroutineScope 74 @Inject lateinit var wallpaperConnectionUtils: WallpaperConnectionUtils 75 76 private var refreshCreativeCategories: Boolean? = null 77 78 private val wallpaperPreviewViewModel: WallpaperPreviewViewModel by viewModels() 79 private val categoriesViewModel: CategoriesViewModel by viewModels() 80 81 private val isNewPickerUi = BaseFlags.get().isNewPickerUi() 82 private val isCategoriesRefactorEnabled = 83 BaseFlags.get().isWallpaperCategoryRefactoringEnabled() 84 85 override fun onCreate(savedInstanceState: Bundle?) { 86 window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS) 87 super.onCreate(savedInstanceState) 88 enforcePortraitForHandheldAndFoldedDisplay() 89 wallpaperPreviewViewModel.updateDisplayConfiguration() 90 wallpaperPreviewViewModel.setIsWallpaperColorPreviewEnabled( 91 !InjectorProvider.getInjector().isCurrentSelectedColorPreset(appContext) 92 ) 93 window.navigationBarColor = Color.TRANSPARENT 94 window.statusBarColor = Color.TRANSPARENT 95 setContentView(R.layout.activity_wallpaper_preview) 96 97 if (isCategoriesRefactorEnabled) { 98 refreshCreativeCategories = intent.getBooleanExtra(SHOULD_CATEGORY_REFRESH, false) 99 } 100 101 val wallpaper: WallpaperModel = 102 if (isNewPickerUi || isCategoriesRefactorEnabled) { 103 val model = 104 if (savedInstanceState != null) { 105 wallpaperPreviewViewModel.wallpaper.value 106 } else { 107 persistentWallpaperModelRepository.wallpaperModel.value 108 ?: intent 109 .getParcelableExtra(EXTRA_WALLPAPER_INFO, WallpaperInfo::class.java) 110 ?.convertToWallpaperModel() 111 } 112 persistentWallpaperModelRepository.cleanup() 113 model 114 } else { 115 intent 116 .getParcelableExtra(EXTRA_WALLPAPER_INFO, WallpaperInfo::class.java) 117 ?.convertToWallpaperModel() 118 } ?: throw IllegalStateException("No wallpaper for previewing") 119 if (savedInstanceState == null) { 120 wallpaperPreviewRepository.setWallpaperModel(wallpaper) 121 } 122 123 val navController = 124 (supportFragmentManager.findFragmentById(R.id.wallpaper_preview_nav_host) 125 as NavHostFragment) 126 .navController 127 val graph = navController.navInflater.inflate(R.navigation.wallpaper_preview_nav_graph) 128 val startDestinationArgs: Bundle? = 129 (wallpaper as? WallpaperModel.LiveWallpaperModel) 130 ?.let { 131 if (it.isNewCreativeWallpaper()) it.getNewCreativeWallpaperArgs() else null 132 } 133 ?.also { 134 // For creating a new creative wallpaper, replace the default start destination 135 // with CreativeEditPreviewFragment. 136 graph.setStartDestination(R.id.creativeEditPreviewFragment) 137 } 138 navController.setGraph(graph, startDestinationArgs) 139 // Fits screen to navbar and statusbar 140 WindowCompat.setDecorFitsSystemWindows(window, ActivityUtils.isSUWMode(this)) 141 val isAssetIdPresent = intent.getBooleanExtra(IS_ASSET_ID_PRESENT, false) 142 wallpaperPreviewViewModel.isNewTask = intent.getBooleanExtra(IS_NEW_TASK, false) 143 val whichPreview = 144 if (isAssetIdPresent) WallpaperConnection.WhichPreview.EDIT_NON_CURRENT 145 else WallpaperConnection.WhichPreview.EDIT_CURRENT 146 wallpaperPreviewViewModel.setWhichPreview(whichPreview) 147 if (wallpaper is WallpaperModel.StaticWallpaperModel) { 148 wallpaper.staticWallpaperData.cropHints?.let { 149 wallpaperPreviewViewModel.setCropHints(it) 150 } 151 } 152 if ( 153 (wallpaper as? WallpaperModel.StaticWallpaperModel)?.downloadableWallpaperData != null 154 ) { 155 liveWallpaperDownloader.initiateDownloadableService( 156 this, 157 wallpaper, 158 registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {}, 159 ) 160 } 161 162 val creativeWallpaperEffectData = 163 (wallpaper as? WallpaperModel.LiveWallpaperModel) 164 ?.creativeWallpaperData 165 ?.creativeWallpaperEffectsData 166 if ( 167 creativeWallpaperEffectData != null && !creativeEffectsRepository.isEffectInitialized() 168 ) { 169 lifecycleScope.launch { 170 creativeEffectsRepository.initializeEffect(creativeWallpaperEffectData) 171 } 172 } else if ( 173 (wallpaper as? WallpaperModel.StaticWallpaperModel)?.imageWallpaperData != null && 174 imageEffectsRepository.areEffectsAvailable() 175 ) { 176 lifecycleScope.launch { 177 imageEffectsRepository.initializeEffect( 178 staticWallpaperModel = wallpaper, 179 onWallpaperModelUpdated = { wallpaper -> 180 wallpaperPreviewRepository.setWallpaperModel(wallpaper) 181 }, 182 ) 183 } 184 } 185 } 186 187 override fun onUpArrowPressed() { 188 onBackPressedDispatcher.onBackPressed() 189 } 190 191 override fun isUpArrowSupported(): Boolean { 192 return !ActivityUtils.isSUWMode(baseContext) 193 } 194 195 override fun onResume() { 196 super.onResume() 197 val isWindowingModeFreeform = 198 resources.configuration.windowConfiguration.windowingMode == WINDOWING_MODE_FREEFORM 199 if (isInMultiWindowMode && !isWindowingModeFreeform) { 200 Toast.makeText(this, R.string.wallpaper_exit_split_screen, Toast.LENGTH_SHORT).show() 201 onBackPressedDispatcher.onBackPressed() 202 } 203 } 204 205 override fun onPause() { 206 super.onPause() 207 208 // When back to main screen user could launch preview again before it's fully destroyed and 209 // it could clean up the repo set by the new launching call, move it earlier to on pause. 210 if (isFinishing) { 211 persistentWallpaperModelRepository.cleanup() 212 } 213 } 214 215 override fun onDestroy() { 216 if (isFinishing) { 217 // ImageEffectsRepositoryImpl is Activity-Retained Scoped, and its injected 218 // EffectsController is Singleton scoped. Therefore, persist state on config change 219 // restart, and only destroy when activity is finishing. 220 imageEffectsRepository.destroy() 221 // CreativeEffectsRepository is Activity-Retained Scoped, and its injected 222 // EffectsController is Singleton scoped. Therefore, persist state on config change 223 // restart, and only destroy when activity is finishing. 224 creativeEffectsRepository.destroy() 225 } 226 liveWallpaperDownloader.cleanup() 227 // TODO(b/333879532): Only disconnect when leaving the Activity without introducing black 228 // preview. If onDestroy is caused by an orientation change, we should keep the connection 229 // to avoid initiating the engines again. 230 // TODO(b/328302105): MainScope ensures the job gets done non-blocking even if the 231 // activity has been destroyed already. Consider making this part of 232 // WallpaperConnectionUtils. 233 mainScope.launch { wallpaperConnectionUtils.disconnectAll(appContext) } 234 235 refreshCreativeCategories?.let { 236 if (it) { 237 categoriesViewModel.refreshCategory() 238 } 239 } 240 241 super.onDestroy() 242 } 243 244 private fun WallpaperInfo.convertToWallpaperModel(): WallpaperModel { 245 return wallpaperModelFactory.getWallpaperModel(appContext, this) 246 } 247 248 companion object { 249 /** 250 * Returns a new [Intent] for the new picker UI that can be used to start 251 * [WallpaperPreviewActivity]. 252 * 253 * @param context application context. 254 * @param isNewTask true to launch at a new task. 255 */ 256 fun newIntent( 257 context: Context, 258 isAssetIdPresent: Boolean, 259 isViewAsHome: Boolean = false, 260 isNewTask: Boolean = false, 261 ): Intent { 262 val isNewPickerUi = BaseFlags.get().isNewPickerUi() 263 val isCategoriesRefactorEnabled = 264 BaseFlags.get().isWallpaperCategoryRefactoringEnabled() 265 if (!(isNewPickerUi || isCategoriesRefactorEnabled)) 266 throw UnsupportedOperationException() 267 val intent = Intent(context.applicationContext, WallpaperPreviewActivity::class.java) 268 if (isNewTask) { 269 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) 270 } 271 intent.putExtra(IS_ASSET_ID_PRESENT, isAssetIdPresent) 272 intent.putExtra(EXTRA_VIEW_AS_HOME, isViewAsHome) 273 intent.putExtra(IS_NEW_TASK, isNewTask) 274 return intent 275 } 276 277 /** 278 * Returns a new [Intent] for the new picker UI that can be used to start 279 * [WallpaperPreviewActivity]. 280 * 281 * @param context application context. 282 * @param isNewTask true to launch at a new task. 283 * @param shouldCategoryRefresh specified the category type 284 */ 285 fun newIntent( 286 context: Context, 287 isAssetIdPresent: Boolean, 288 isViewAsHome: Boolean = false, 289 isNewTask: Boolean = false, 290 shouldCategoryRefresh: Boolean, 291 ): Intent { 292 val isNewPickerUi = BaseFlags.get().isNewPickerUi() 293 val isCategoriesRefactorEnabled = 294 BaseFlags.get().isWallpaperCategoryRefactoringEnabled() 295 if (!(isNewPickerUi || isCategoriesRefactorEnabled)) 296 throw UnsupportedOperationException() 297 val intent = Intent(context.applicationContext, WallpaperPreviewActivity::class.java) 298 if (isNewTask) { 299 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) 300 } 301 intent.putExtra(IS_ASSET_ID_PRESENT, isAssetIdPresent) 302 intent.putExtra(EXTRA_VIEW_AS_HOME, isViewAsHome) 303 intent.putExtra(IS_NEW_TASK, isNewTask) 304 intent.putExtra(SHOULD_CATEGORY_REFRESH, shouldCategoryRefresh) 305 return intent 306 } 307 308 /** 309 * Returns a new [Intent] that can be used to start [WallpaperPreviewActivity]. 310 * 311 * @param context application context. 312 * @param wallpaperInfo selected by user for editing preview. 313 * @param isNewTask true to launch at a new task. 314 * 315 * TODO(b/291761856): Use wallpaper model to replace wallpaper info. 316 */ 317 fun newIntent( 318 context: Context, 319 wallpaperInfo: WallpaperInfo, 320 isAssetIdPresent: Boolean, 321 isViewAsHome: Boolean = false, 322 isNewTask: Boolean = false, 323 ): Intent { 324 val intent = Intent(context.applicationContext, WallpaperPreviewActivity::class.java) 325 if (isNewTask) { 326 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) 327 } 328 intent.putExtra(EXTRA_WALLPAPER_INFO, wallpaperInfo) 329 intent.putExtra(IS_ASSET_ID_PRESENT, isAssetIdPresent) 330 intent.putExtra(EXTRA_VIEW_AS_HOME, isViewAsHome) 331 intent.putExtra(IS_NEW_TASK, isNewTask) 332 return intent 333 } 334 335 /** 336 * Returns a new [Intent] that can be used to start [WallpaperPreviewActivity]. 337 * 338 * @param context application context. 339 * @param wallpaperInfo selected by user for editing preview. 340 * @param isNewTask true to launch at a new task. 341 * @param shouldRefreshCategory specifies the type of category this wallpaper belongs 342 * 343 * TODO(b/291761856): Use wallpaper model to replace wallpaper info. 344 */ 345 fun newIntent( 346 context: Context, 347 wallpaperInfo: WallpaperInfo, 348 isAssetIdPresent: Boolean, 349 isViewAsHome: Boolean = false, 350 isNewTask: Boolean = false, 351 shouldRefreshCategory: Boolean, 352 ): Intent { 353 val intent = Intent(context.applicationContext, WallpaperPreviewActivity::class.java) 354 if (isNewTask) { 355 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) 356 } 357 intent.putExtra(EXTRA_WALLPAPER_INFO, wallpaperInfo) 358 intent.putExtra(IS_ASSET_ID_PRESENT, isAssetIdPresent) 359 intent.putExtra(EXTRA_VIEW_AS_HOME, isViewAsHome) 360 intent.putExtra(IS_NEW_TASK, isNewTask) 361 intent.putExtra(SHOULD_CATEGORY_REFRESH, shouldRefreshCategory) 362 return intent 363 } 364 365 /** 366 * Returns a new [Intent] that can be used to start [WallpaperPreviewActivity], explicitly 367 * propagating any permissions on the wallpaper data to the new [Intent]. 368 * 369 * @param context application context. 370 * @param wallpaperInfo selected by user for editing preview. 371 * @param isNewTask true to launch at a new task. 372 * 373 * TODO(b/291761856): Use wallpaper model to replace wallpaper info. 374 */ 375 fun newIntent( 376 context: Context, 377 originalIntent: Intent, 378 isAssetIdPresent: Boolean, 379 isViewAsHome: Boolean = false, 380 isNewTask: Boolean = false, 381 ): Intent { 382 val data = originalIntent.data 383 val intent = 384 newIntent( 385 context, 386 ImageWallpaperInfo(data), 387 isAssetIdPresent, 388 isViewAsHome, 389 isNewTask, 390 ) 391 // Both these lines are required for permission propagation 392 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 393 intent.setData(data) 394 return intent 395 } 396 397 private fun WallpaperModel.LiveWallpaperModel.getNewCreativeWallpaperArgs() = 398 Bundle().apply { 399 putParcelable( 400 SmallPreviewFragment.ARG_EDIT_INTENT, 401 liveWallpaperData.getEditActivityIntent(true), 402 ) 403 } 404 } 405 406 /** 407 * If the display is a handheld display or a folded display from a foldable, we enforce the 408 * activity to be portrait. 409 * 410 * This method should be called upon initialization of this activity, and whenever there is a 411 * configuration change. 412 */ 413 private fun enforcePortraitForHandheldAndFoldedDisplay() { 414 val wantedOrientation = 415 if (displayUtils.isLargeScreenOrUnfoldedDisplay(this)) 416 ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED 417 else ActivityInfo.SCREEN_ORIENTATION_PORTRAIT 418 if (requestedOrientation != wantedOrientation) { 419 requestedOrientation = wantedOrientation 420 } 421 } 422 } 423