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.fragment
17 
18 import android.app.Activity
19 import android.app.ActivityOptions
20 import android.app.AlertDialog
21 import android.content.Context
22 import android.content.Intent
23 import android.os.Bundle
24 import android.transition.Slide
25 import android.view.Gravity
26 import android.view.LayoutInflater
27 import android.view.View
28 import android.view.ViewGroup
29 import android.widget.Toast
30 import androidx.activity.addCallback
31 import androidx.activity.result.ActivityResultLauncher
32 import androidx.activity.result.contract.ActivityResultContract
33 import androidx.constraintlayout.motion.widget.MotionLayout
34 import androidx.core.content.ContextCompat
35 import androidx.core.view.doOnPreDraw
36 import androidx.fragment.app.activityViewModels
37 import androidx.lifecycle.Lifecycle
38 import androidx.lifecycle.lifecycleScope
39 import androidx.lifecycle.repeatOnLifecycle
40 import androidx.navigation.fragment.FragmentNavigatorExtras
41 import androidx.navigation.fragment.findNavController
42 import androidx.transition.Transition
43 import com.android.wallpaper.R
44 import com.android.wallpaper.config.BaseFlags
45 import com.android.wallpaper.model.Screen
46 import com.android.wallpaper.module.logging.UserEventLogger
47 import com.android.wallpaper.picker.AppbarFragment
48 import com.android.wallpaper.picker.TrampolinePickerActivity
49 import com.android.wallpaper.picker.customization.ui.CustomizationPickerFragment2
50 import com.android.wallpaper.picker.di.modules.MainDispatcher
51 import com.android.wallpaper.picker.preview.ui.WallpaperPreviewActivity
52 import com.android.wallpaper.picker.preview.ui.binder.ApplyWallpaperScreenBinder
53 import com.android.wallpaper.picker.preview.ui.binder.DualPreviewSelectorBinder
54 import com.android.wallpaper.picker.preview.ui.binder.PreviewActionsBinder
55 import com.android.wallpaper.picker.preview.ui.binder.PreviewSelectorBinder
56 import com.android.wallpaper.picker.preview.ui.binder.SetWallpaperButtonBinder
57 import com.android.wallpaper.picker.preview.ui.binder.SetWallpaperProgressDialogBinder
58 import com.android.wallpaper.picker.preview.ui.binder.SmallPreviewScreenBinder
59 import com.android.wallpaper.picker.preview.ui.util.AnimationUtil
60 import com.android.wallpaper.picker.preview.ui.util.ImageEffectDialogUtil
61 import com.android.wallpaper.picker.preview.ui.view.DualPreviewViewPager
62 import com.android.wallpaper.picker.preview.ui.view.PreviewActionFloatingSheet
63 import com.android.wallpaper.picker.preview.ui.view.PreviewActionGroup
64 import com.android.wallpaper.picker.preview.ui.view.PreviewTabs
65 import com.android.wallpaper.picker.preview.ui.viewmodel.Action
66 import com.android.wallpaper.picker.preview.ui.viewmodel.WallpaperPreviewViewModel
67 import com.android.wallpaper.util.DisplayUtils
68 import com.android.wallpaper.util.LaunchSourceUtils.LAUNCH_SOURCE_LAUNCHER
69 import com.android.wallpaper.util.LaunchSourceUtils.LAUNCH_SOURCE_SETTINGS_HOMEPAGE
70 import com.android.wallpaper.util.LaunchSourceUtils.WALLPAPER_LAUNCH_SOURCE
71 import com.android.wallpaper.util.wallpaperconnection.WallpaperConnectionUtils
72 import dagger.hilt.android.AndroidEntryPoint
73 import dagger.hilt.android.qualifiers.ApplicationContext
74 import javax.inject.Inject
75 import kotlinx.coroutines.CompletableDeferred
76 import kotlinx.coroutines.CoroutineScope
77 import kotlinx.coroutines.launch
78 
79 /**
80  * This fragment displays the preview of the selected wallpaper on all available workspaces and
81  * device displays.
82  */
83 @AndroidEntryPoint(AppbarFragment::class)
84 class SmallPreviewFragment : Hilt_SmallPreviewFragment() {
85 
86     @Inject @ApplicationContext lateinit var appContext: Context
87     @Inject @MainDispatcher lateinit var mainScope: CoroutineScope
88     @Inject lateinit var displayUtils: DisplayUtils
89     @Inject lateinit var logger: UserEventLogger
90     @Inject lateinit var imageEffectDialogUtil: ImageEffectDialogUtil
91     @Inject lateinit var wallpaperConnectionUtils: WallpaperConnectionUtils
92 
93     private lateinit var currentView: View
94     private lateinit var shareActivityResult: ActivityResultLauncher<Intent>
95 
96     private val wallpaperPreviewViewModel by activityViewModels<WallpaperPreviewViewModel>()
97     private val isFirstBindingDeferred = CompletableDeferred<Boolean>()
98 
99     /**
100      * True if the view of this fragment is destroyed from the current or previous lifecycle.
101      *
102      * Null if it's the first life cycle, and false if the view has not been destroyed.
103      *
104      * Read-only during the first half of the lifecycle (when starting a fragment).
105      */
106     private var isViewDestroyed: Boolean? = null
107 
108     private var setWallpaperProgressDialog: AlertDialog? = null
109 
110     override fun onCreate(savedInstanceState: Bundle?) {
111         super.onCreate(savedInstanceState)
112         exitTransition = AnimationUtil.getFastFadeOutTransition()
113         reenterTransition = AnimationUtil.getFastFadeInTransition()
114     }
115 
116     override fun onCreateView(
117         inflater: LayoutInflater,
118         container: ViewGroup?,
119         savedInstanceState: Bundle?,
120     ): View {
121         val isNewPickerUi = BaseFlags.get().isNewPickerUi()
122         val isFoldable = displayUtils.hasMultiInternalDisplays()
123         postponeEnterTransition()
124         currentView =
125             inflater.inflate(
126                 if (isNewPickerUi) {
127                     if (isFoldable) R.layout.fragment_small_preview_foldable2
128                     else R.layout.fragment_small_preview_handheld2
129                 } else {
130                     if (isFoldable) R.layout.fragment_small_preview_foldable
131                     else R.layout.fragment_small_preview_handheld
132                 },
133                 container,
134                 /* attachToRoot= */ false,
135             )
136         val smallPreview =
137             if (isNewPickerUi) currentView.findViewById<MotionLayout>(R.id.small_preview_container)
138             else null
139         val previewPager =
140             if (isNewPickerUi) currentView.findViewById<MotionLayout>(R.id.preview_pager) else null
141         previewPager?.let { setUpTransitionListener(it) }
142         if (isNewPickerUi) {
143             requireActivity().onBackPressedDispatcher.let {
144                 it.addCallback {
145                     isEnabled = wallpaperPreviewViewModel.handleBackPressed()
146                     if (!isEnabled) it.onBackPressed()
147                 }
148             }
149         }
150 
151         setUpToolbar(currentView, /* upArrow= */ true, /* transparentToolbar= */ true)
152         bindScreenPreview(currentView, isFirstBindingDeferred, isFoldable, isNewPickerUi)
153         bindPreviewActions(currentView, smallPreview)
154 
155         if (isNewPickerUi) {
156             /**
157              * We need to keep the reference shortly, because the activity will be forced to restart
158              * due to the theme color update from the system wallpaper change. The activityReference
159              * is used to kill [WallpaperPreviewActivity].
160              */
161             val activityReference = activity
162             checkNotNull(previewPager)
163             ApplyWallpaperScreenBinder.bind(
164                 applyButton = previewPager.requireViewById(R.id.apply_button),
165                 cancelButton = previewPager.requireViewById(R.id.cancel_button),
166                 homeCheckbox = previewPager.requireViewById(R.id.home_checkbox),
167                 lockCheckbox = previewPager.requireViewById(R.id.lock_checkbox),
168                 viewModel = wallpaperPreviewViewModel,
169                 lifecycleOwner = viewLifecycleOwner,
170                 mainScope = mainScope,
171             ) {
172                 Toast.makeText(
173                         context,
174                         R.string.wallpaper_set_successfully_message,
175                         Toast.LENGTH_SHORT,
176                     )
177                     .show()
178                 if (activityReference != null) {
179                     if (wallpaperPreviewViewModel.isNewTask) {
180                         activityReference.window?.exitTransition = Slide(Gravity.END)
181                         val intent = Intent(activityReference, TrampolinePickerActivity::class.java)
182                         intent.setFlags(
183                             Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
184                         )
185                         intent.putExtra(
186                             WALLPAPER_LAUNCH_SOURCE,
187                             if (wallpaperPreviewViewModel.isViewAsHome) LAUNCH_SOURCE_LAUNCHER
188                             else LAUNCH_SOURCE_SETTINGS_HOMEPAGE,
189                         )
190                         activityReference.startActivity(
191                             intent,
192                             ActivityOptions.makeSceneTransitionAnimation(activityReference)
193                                 .toBundle(),
194                         )
195                     } else {
196                         activityReference.setResult(Activity.RESULT_OK)
197                     }
198                     activityReference.finish()
199                 }
200             }
201         } else {
202             SetWallpaperButtonBinder.bind(
203                 button = currentView.requireViewById(R.id.button_set_wallpaper),
204                 viewModel = wallpaperPreviewViewModel,
205                 lifecycleOwner = viewLifecycleOwner,
206             ) {
207                 findNavController().navigate(R.id.setWallpaperDialog)
208             }
209         }
210 
211         val dialogView = inflater.inflate(R.layout.set_wallpaper_progress_dialog_view, null)
212         setWallpaperProgressDialog =
213             AlertDialog.Builder(requireActivity()).setView(dialogView).create()
214         SetWallpaperProgressDialogBinder.bind(
215             viewModel = wallpaperPreviewViewModel,
216             lifecycleOwner = viewLifecycleOwner,
217         ) { visible ->
218             setWallpaperProgressDialog?.let { if (visible) it.show() else it.dismiss() }
219         }
220 
221         currentView.doOnPreDraw {
222             // FullPreviewConfigViewModel not being null indicates that we are navigated to small
223             // preview from the full preview, and therefore should play the shared element re-enter
224             // animation. Reset it after views are finished binding.
225             wallpaperPreviewViewModel.resetFullPreviewConfigViewModel()
226             startPostponedEnterTransition()
227         }
228 
229         shareActivityResult =
230             registerForActivityResult(
231                 object : ActivityResultContract<Intent, Int>() {
232                     override fun createIntent(context: Context, input: Intent): Intent {
233                         return input
234                     }
235 
236                     override fun parseResult(resultCode: Int, intent: Intent?): Int {
237                         return resultCode
238                     }
239                 }
240             ) {
241                 currentView
242                     .findViewById<PreviewActionGroup>(R.id.action_button_group)
243                     ?.setIsChecked(Action.SHARE, false)
244             }
245 
246         return currentView
247     }
248 
249     override fun onViewStateRestored(savedInstanceState: Bundle?) {
250         super.onViewStateRestored(savedInstanceState)
251         isFirstBindingDeferred.complete(savedInstanceState == null)
252     }
253 
254     override fun onStart() {
255         super.onStart()
256         // Reinitialize the preview tab motion. If navigating up back to this fragment happened
257         // before the transition finished, the lifecycle begins at onStart without recreating the
258         // preview tabs,
259         isViewDestroyed?.let {
260             if (!it) {
261                 currentView
262                     .findViewById<PreviewTabs>(R.id.preview_tabs_container)
263                     ?.resetTransition(wallpaperPreviewViewModel.getSmallPreviewTabIndex())
264             }
265         }
266     }
267 
268     override fun onStop() {
269         super.onStop()
270         // onStop won't destroy view
271         isViewDestroyed = false
272     }
273 
274     override fun onDestroyView() {
275         super.onDestroyView()
276         setWallpaperProgressDialog?.dismiss()
277         isViewDestroyed = true
278     }
279 
280     override fun getDefaultTitle(): CharSequence {
281         return getString(R.string.preview)
282     }
283 
284     override fun getToolbarTextColor(): Int {
285         return ContextCompat.getColor(requireContext(), R.color.system_on_surface)
286     }
287 
288     private fun setUpTransitionListener(previewPager: MotionLayout) {
289         previewPager.addTransitionListener(
290             object : CustomizationPickerFragment2.EmptyTransitionListener {
291                 override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
292                     if (
293                         currentId == R.id.lock_preview_selected ||
294                             currentId == R.id.home_preview_selected
295                     ) {
296                         // When user swipes to lock or home screen, we need to update the state of
297                         // the selected tab in the view model
298                         wallpaperPreviewViewModel.setSmallPreviewSelectedTab(
299                             if (currentId == R.id.lock_preview_selected) Screen.LOCK_SCREEN
300                             else Screen.HOME_SCREEN
301                         )
302                     } else if (currentId == R.id.apply_wallpaper_preview_only) {
303                         // When transition to state of apply wallpaper preview only, it should
304                         // always proceed to transition to the apply wallpaper all state to also
305                         // fade in the action buttons at the bottom.
306                         previewPager.transitionToState(R.id.apply_wallpaper_all)
307                     }
308                 }
309             }
310         )
311     }
312 
313     private fun bindScreenPreview(
314         view: View,
315         isFirstBindingDeferred: CompletableDeferred<Boolean>,
316         isFoldable: Boolean,
317         isNewPickerUi: Boolean,
318     ) {
319         val tabs = view.findViewById<PreviewTabs>(R.id.preview_tabs_container)
320 
321         if (isNewPickerUi) {
322             SmallPreviewScreenBinder.bind(
323                 applicationContext = appContext,
324                 mainScope = mainScope,
325                 lifecycleOwner = viewLifecycleOwner,
326                 fragmentLayout = view as MotionLayout,
327                 viewModel = wallpaperPreviewViewModel,
328                 previewDisplaySize = displayUtils.getRealSize(displayUtils.getWallpaperDisplay()),
329                 transition = (reenterTransition as Transition?),
330                 transitionConfig = wallpaperPreviewViewModel.fullPreviewConfigViewModel.value,
331                 wallpaperConnectionUtils = wallpaperConnectionUtils,
332                 isFirstBindingDeferred = isFirstBindingDeferred,
333                 isFoldable = isFoldable,
334             ) { sharedElement ->
335                 val extras =
336                     FragmentNavigatorExtras(sharedElement to FULL_PREVIEW_SHARED_ELEMENT_ID)
337                 // Set to false on small-to-full preview transition to remove surfaceView jank.
338                 (view as ViewGroup).isTransitionGroup = false
339                 findNavController().let {
340                     if (it.currentDestination?.id == R.id.smallPreviewFragment) {
341                         it.navigate(
342                             resId = R.id.action_smallPreviewFragment_to_fullPreviewFragment,
343                             args = null,
344                             navOptions = null,
345                             navigatorExtras = extras,
346                         )
347                     }
348                 }
349             }
350         } else {
351             if (isFoldable) {
352                 val dualPreviewView: DualPreviewViewPager =
353                     view.requireViewById(R.id.pager_previews)
354 
355                 DualPreviewSelectorBinder.bind(
356                     tabs,
357                     dualPreviewView,
358                     wallpaperPreviewViewModel,
359                     appContext,
360                     mainScope,
361                     viewLifecycleOwner,
362                     (reenterTransition as Transition?),
363                     wallpaperPreviewViewModel.fullPreviewConfigViewModel.value,
364                     wallpaperConnectionUtils,
365                     isFirstBindingDeferred,
366                 ) { sharedElement ->
367                     val extras =
368                         FragmentNavigatorExtras(sharedElement to FULL_PREVIEW_SHARED_ELEMENT_ID)
369                     // Set to false on small-to-full preview transition to remove surfaceView jank.
370                     (view as ViewGroup).isTransitionGroup = false
371                     findNavController()
372                         .navigate(
373                             resId = R.id.action_smallPreviewFragment_to_fullPreviewFragment,
374                             args = null,
375                             navOptions = null,
376                             navigatorExtras = extras,
377                         )
378                 }
379             } else {
380                 PreviewSelectorBinder.bind(
381                     tabs,
382                     view.findViewById(R.id.pager_previews),
383                     displayUtils.getRealSize(displayUtils.getWallpaperDisplay()),
384                     wallpaperPreviewViewModel,
385                     appContext,
386                     mainScope,
387                     viewLifecycleOwner,
388                     (reenterTransition as Transition?),
389                     wallpaperPreviewViewModel.fullPreviewConfigViewModel.value,
390                     wallpaperConnectionUtils,
391                     isFirstBindingDeferred,
392                 ) { sharedElement ->
393                     val extras =
394                         FragmentNavigatorExtras(sharedElement to FULL_PREVIEW_SHARED_ELEMENT_ID)
395                     // Set to false on small-to-full preview transition to remove surfaceView jank.
396                     (view as ViewGroup).isTransitionGroup = false
397                     findNavController().let {
398                         if (it.currentDestination?.id == R.id.smallPreviewFragment) {
399                             it.navigate(
400                                 resId = R.id.action_smallPreviewFragment_to_fullPreviewFragment,
401                                 args = null,
402                                 navOptions = null,
403                                 navigatorExtras = extras,
404                             )
405                         }
406                     }
407                 }
408             }
409         }
410 
411         viewLifecycleOwner.lifecycleScope.launch {
412             viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
413                 // Always reset isTransitionGroup value on start for the edge case that the
414                 // navigation is cancelled and the fragment resumes.
415                 (view as ViewGroup).isTransitionGroup = true
416             }
417         }
418     }
419 
420     private fun bindPreviewActions(view: View, smallPreview: MotionLayout?) {
421         val actionButtonGroup = view.findViewById<PreviewActionGroup>(R.id.action_button_group)
422         val floatingSheet = view.findViewById<PreviewActionFloatingSheet>(R.id.floating_sheet)
423         if (actionButtonGroup == null || floatingSheet == null) {
424             return
425         }
426 
427         PreviewActionsBinder.bind(
428             actionGroup = actionButtonGroup,
429             floatingSheet = floatingSheet,
430             smallPreview = smallPreview,
431             previewViewModel = wallpaperPreviewViewModel,
432             actionsViewModel = wallpaperPreviewViewModel.previewActionsViewModel,
433             deviceDisplayType = displayUtils.getCurrentDisplayType(requireActivity()),
434             activity = requireActivity(),
435             lifecycleOwner = viewLifecycleOwner,
436             logger = logger,
437             imageEffectDialogUtil = imageEffectDialogUtil,
438             onNavigateToEditScreen = { navigateToEditScreen(it) },
439             onStartShareActivity = { shareActivityResult.launch(it) },
440         )
441     }
442 
443     private fun navigateToEditScreen(intent: Intent) {
444         findNavController()
445             .navigate(
446                 resId = R.id.action_smallPreviewFragment_to_creativeEditPreviewFragment,
447                 args = Bundle().apply { putParcelable(ARG_EDIT_INTENT, intent) },
448                 navOptions = null,
449                 navigatorExtras = null,
450             )
451     }
452 
453     companion object {
454         const val SMALL_PREVIEW_HOME_SHARED_ELEMENT_ID = "small_preview_home"
455         const val SMALL_PREVIEW_LOCK_SHARED_ELEMENT_ID = "small_preview_lock"
456         const val SMALL_PREVIEW_HOME_FOLDED_SHARED_ELEMENT_ID = "small_preview_home_folded"
457         const val SMALL_PREVIEW_HOME_UNFOLDED_SHARED_ELEMENT_ID = "small_preview_home_unfolded"
458         const val SMALL_PREVIEW_LOCK_FOLDED_SHARED_ELEMENT_ID = "small_preview_lock_folded"
459         const val SMALL_PREVIEW_LOCK_UNFOLDED_SHARED_ELEMENT_ID = "small_preview_lock_unfolded"
460         const val FULL_PREVIEW_SHARED_ELEMENT_ID = "full_preview"
461         const val ARG_EDIT_INTENT = "arg_edit_intent"
462     }
463 }
464