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