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.content.Context
19 import android.content.Intent
20 import android.os.Bundle
21 import android.view.LayoutInflater
22 import android.view.View
23 import android.view.ViewGroup
24 import androidx.activity.result.ActivityResultLauncher
25 import androidx.activity.result.contract.ActivityResultContract
26 import androidx.core.content.ContextCompat
27 import androidx.core.view.doOnPreDraw
28 import androidx.fragment.app.activityViewModels
29 import androidx.lifecycle.Lifecycle
30 import androidx.lifecycle.lifecycleScope
31 import androidx.lifecycle.repeatOnLifecycle
32 import androidx.navigation.fragment.FragmentNavigatorExtras
33 import androidx.navigation.fragment.findNavController
34 import androidx.transition.Transition
35 import com.android.wallpaper.R
36 import com.android.wallpaper.R.id.preview_tabs_container
37 import com.android.wallpaper.module.logging.UserEventLogger
38 import com.android.wallpaper.picker.AppbarFragment
39 import com.android.wallpaper.picker.preview.ui.binder.DualPreviewSelectorBinder
40 import com.android.wallpaper.picker.preview.ui.binder.PreviewActionsBinder
41 import com.android.wallpaper.picker.preview.ui.binder.PreviewSelectorBinder
42 import com.android.wallpaper.picker.preview.ui.binder.SetWallpaperButtonBinder
43 import com.android.wallpaper.picker.preview.ui.binder.SetWallpaperProgressDialogBinder
44 import com.android.wallpaper.picker.preview.ui.util.AnimationUtil
45 import com.android.wallpaper.picker.preview.ui.util.ImageEffectDialogUtil
46 import com.android.wallpaper.picker.preview.ui.view.DualPreviewViewPager
47 import com.android.wallpaper.picker.preview.ui.view.PreviewActionGroup
48 import com.android.wallpaper.picker.preview.ui.view.PreviewTabs
49 import com.android.wallpaper.picker.preview.ui.viewmodel.Action
50 import com.android.wallpaper.picker.preview.ui.viewmodel.WallpaperPreviewViewModel
51 import com.android.wallpaper.util.DisplayUtils
52 import dagger.hilt.android.AndroidEntryPoint
53 import dagger.hilt.android.qualifiers.ApplicationContext
54 import javax.inject.Inject
55 import kotlinx.coroutines.launch
56 
57 /**
58  * This fragment displays the preview of the selected wallpaper on all available workspaces and
59  * device displays.
60  */
61 @AndroidEntryPoint(AppbarFragment::class)
62 class SmallPreviewFragment : Hilt_SmallPreviewFragment() {
63 
64     @Inject @ApplicationContext lateinit var appContext: Context
65     @Inject lateinit var displayUtils: DisplayUtils
66     @Inject lateinit var logger: UserEventLogger
67     @Inject lateinit var imageEffectDialogUtil: ImageEffectDialogUtil
68 
69     private lateinit var currentView: View
70     private lateinit var shareActivityResult: ActivityResultLauncher<Intent>
71 
72     private val wallpaperPreviewViewModel by activityViewModels<WallpaperPreviewViewModel>()
73 
74     /**
75      * True if the view of this fragment is destroyed from the current or previous lifecycle.
76      *
77      * Null if it's the first life cycle, and false if the view has not been destroyed.
78      *
79      * Read-only during the first half of the lifecycle (when starting a fragment).
80      */
81     private var isViewDestroyed: Boolean? = null
82 
83     override fun onCreate(savedInstanceState: Bundle?) {
84         super.onCreate(savedInstanceState)
85         exitTransition = AnimationUtil.getFastFadeOutTransition()
86         reenterTransition = AnimationUtil.getFastFadeInTransition()
87     }
88 
89     override fun onCreateView(
90         inflater: LayoutInflater,
91         container: ViewGroup?,
92         savedInstanceState: Bundle?,
93     ): View {
94         postponeEnterTransition()
95         currentView =
96             inflater.inflate(
97                 if (displayUtils.hasMultiInternalDisplays())
98                     R.layout.fragment_small_preview_foldable
99                 else R.layout.fragment_small_preview_handheld,
100                 container,
101                 false,
102             )
103         setUpToolbar(currentView, /* upArrow= */ true, /* transparentToolbar= */ true)
104         bindPreviewActions(currentView)
105 
106         SetWallpaperButtonBinder.bind(
107             button = currentView.requireViewById(R.id.button_set_wallpaper),
108             viewModel = wallpaperPreviewViewModel,
109             lifecycleOwner = viewLifecycleOwner,
110         ) {
111             findNavController().navigate(R.id.setWallpaperDialog)
112         }
113 
114         SetWallpaperProgressDialogBinder.bind(
115             viewModel = wallpaperPreviewViewModel,
116             activity = requireActivity(),
117             lifecycleOwner = viewLifecycleOwner,
118         )
119 
120         currentView.doOnPreDraw {
121             // FullPreviewConfigViewModel not being null indicates that we are navigated to small
122             // preview from the full preview, and therefore should play the shared element re-enter
123             // animation. Reset it after views are finished binding.
124             wallpaperPreviewViewModel.resetFullPreviewConfigViewModel()
125             startPostponedEnterTransition()
126         }
127 
128         shareActivityResult =
129             registerForActivityResult(
130                 object : ActivityResultContract<Intent, Int>() {
131                     override fun createIntent(context: Context, input: Intent): Intent {
132                         return input
133                     }
134 
135                     override fun parseResult(resultCode: Int, intent: Intent?): Int {
136                         return resultCode
137                     }
138                 },
139             ) {
140                 currentView
141                     .findViewById<PreviewActionGroup>(R.id.action_button_group)
142                     ?.setIsChecked(Action.SHARE, false)
143             }
144 
145         return currentView
146     }
147 
148     override fun onViewStateRestored(savedInstanceState: Bundle?) {
149         super.onViewStateRestored(savedInstanceState)
150         bindScreenPreview(currentView, isFirstBinding = savedInstanceState == null)
151     }
152 
153     override fun onStart() {
154         super.onStart()
155         // Reinitialize the preview tab motion. If navigating up back to this fragment happened
156         // before the transition finished, the lifecycle begins at onStart without recreating the
157         // preview tabs,
158         isViewDestroyed?.let {
159             if (!it) {
160                 currentView
161                     .requireViewById<PreviewTabs>(preview_tabs_container)
162                     .resetTransition(wallpaperPreviewViewModel.getSmallPreviewTabIndex())
163             }
164         }
165     }
166 
167     override fun onStop() {
168         super.onStop()
169         // onStop won't destroy view
170         isViewDestroyed = false
171     }
172 
173     override fun onDestroyView() {
174         super.onDestroyView()
175         isViewDestroyed = true
176     }
177 
178     override fun getDefaultTitle(): CharSequence {
179         return getString(R.string.preview)
180     }
181 
182     override fun getToolbarTextColor(): Int {
183         return ContextCompat.getColor(requireContext(), R.color.system_on_surface)
184     }
185 
186     private fun bindScreenPreview(view: View, isFirstBinding: Boolean) {
187         val currentNavDestId = checkNotNull(findNavController().currentDestination?.id)
188         val tabs = view.requireViewById<PreviewTabs>(preview_tabs_container)
189         if (displayUtils.hasMultiInternalDisplays()) {
190             val dualPreviewView: DualPreviewViewPager =
191                 view.requireViewById(R.id.dual_preview_pager)
192 
193             DualPreviewSelectorBinder.bind(
194                 tabs,
195                 dualPreviewView,
196                 wallpaperPreviewViewModel,
197                 appContext,
198                 viewLifecycleOwner,
199                 currentNavDestId,
200                 (reenterTransition as Transition?),
201                 wallpaperPreviewViewModel.fullPreviewConfigViewModel.value,
202                 isFirstBinding,
203             ) { sharedElement ->
204                 val extras =
205                     FragmentNavigatorExtras(sharedElement to FULL_PREVIEW_SHARED_ELEMENT_ID)
206                 // Set to false on small-to-full preview transition to remove surfaceView jank.
207                 (view as ViewGroup).isTransitionGroup = false
208                 findNavController()
209                     .navigate(
210                         resId = R.id.action_smallPreviewFragment_to_fullPreviewFragment,
211                         args = null,
212                         navOptions = null,
213                         navigatorExtras = extras
214                     )
215             }
216         } else {
217             PreviewSelectorBinder.bind(
218                 tabs,
219                 view.requireViewById(R.id.pager_previews),
220                 displayUtils.getRealSize(displayUtils.getWallpaperDisplay()),
221                 wallpaperPreviewViewModel,
222                 appContext,
223                 viewLifecycleOwner,
224                 currentNavDestId,
225                 (reenterTransition as Transition?),
226                 wallpaperPreviewViewModel.fullPreviewConfigViewModel.value,
227                 isFirstBinding,
228             ) { sharedElement ->
229                 val extras =
230                     FragmentNavigatorExtras(sharedElement to FULL_PREVIEW_SHARED_ELEMENT_ID)
231                 // Set to false on small-to-full preview transition to remove surfaceView jank.
232                 (view as ViewGroup).isTransitionGroup = false
233                 findNavController()
234                     .navigate(
235                         resId = R.id.action_smallPreviewFragment_to_fullPreviewFragment,
236                         args = null,
237                         navOptions = null,
238                         navigatorExtras = extras
239                     )
240             }
241         }
242 
243         viewLifecycleOwner.lifecycleScope.launch {
244             viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
245                 // Always reset isTransitionGroup value on start for the edge case that the
246                 // navigation is cancelled and the fragment resumes.
247                 (view as ViewGroup).isTransitionGroup = true
248             }
249         }
250     }
251 
252     private fun bindPreviewActions(view: View) {
253         PreviewActionsBinder.bind(
254             actionGroup = view.requireViewById(R.id.action_button_group),
255             floatingSheet = view.requireViewById(R.id.floating_sheet),
256             previewViewModel = wallpaperPreviewViewModel,
257             actionsViewModel = wallpaperPreviewViewModel.previewActionsViewModel,
258             deviceDisplayType = displayUtils.getCurrentDisplayType(requireActivity()),
259             activity = requireActivity(),
260             lifecycleOwner = viewLifecycleOwner,
261             logger = logger,
262             imageEffectDialogUtil = imageEffectDialogUtil,
263             onNavigateToEditScreen = { navigateToEditScreen(it) },
264             onStartShareActivity = { shareActivityResult.launch(it) },
265         )
266     }
267 
268     private fun navigateToEditScreen(intent: Intent) {
269         findNavController()
270             .navigate(
271                 resId = R.id.action_smallPreviewFragment_to_creativeEditPreviewFragment,
272                 args = Bundle().apply { putParcelable(ARG_EDIT_INTENT, intent) },
273                 navOptions = null,
274                 navigatorExtras = null,
275             )
276     }
277 
278     companion object {
279         const val SMALL_PREVIEW_HOME_SHARED_ELEMENT_ID = "small_preview_home"
280         const val SMALL_PREVIEW_LOCK_SHARED_ELEMENT_ID = "small_preview_lock"
281         const val SMALL_PREVIEW_HOME_FOLDED_SHARED_ELEMENT_ID = "small_preview_home_folded"
282         const val SMALL_PREVIEW_HOME_UNFOLDED_SHARED_ELEMENT_ID = "small_preview_home_unfolded"
283         const val SMALL_PREVIEW_LOCK_FOLDED_SHARED_ELEMENT_ID = "small_preview_lock_folded"
284         const val SMALL_PREVIEW_LOCK_UNFOLDED_SHARED_ELEMENT_ID = "small_preview_lock_unfolded"
285         const val FULL_PREVIEW_SHARED_ELEMENT_ID = "full_preview"
286         const val ARG_EDIT_INTENT = "arg_edit_intent"
287     }
288 }
289