• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.customization.ui
18 
19 import android.content.ComponentName
20 import android.content.Intent
21 import android.graphics.Color
22 import android.os.Bundle
23 import android.provider.Settings
24 import android.view.LayoutInflater
25 import android.view.SurfaceView
26 import android.view.View
27 import android.view.ViewGroup
28 import android.view.ViewGroup.MarginLayoutParams
29 import android.widget.Button
30 import android.widget.FrameLayout
31 import android.widget.LinearLayout
32 import android.widget.TextView
33 import android.widget.Toolbar
34 import androidx.activity.OnBackPressedCallback
35 import androidx.activity.addCallback
36 import androidx.activity.result.contract.ActivityResultContracts
37 import androidx.constraintlayout.motion.widget.MotionLayout
38 import androidx.constraintlayout.widget.ConstraintLayout
39 import androidx.constraintlayout.widget.ConstraintSet
40 import androidx.core.view.ViewCompat
41 import androidx.core.view.WindowInsetsCompat
42 import androidx.core.view.doOnPreDraw
43 import androidx.fragment.app.commit
44 import androidx.fragment.app.replace
45 import androidx.fragment.app.viewModels
46 import androidx.transition.Transition
47 import com.android.customization.picker.clock.ui.view.ClockViewFactory
48 import com.android.wallpaper.R
49 import com.android.wallpaper.model.Screen
50 import com.android.wallpaper.model.Screen.HOME_SCREEN
51 import com.android.wallpaper.model.Screen.LOCK_SCREEN
52 import com.android.wallpaper.module.LargeScreenMultiPanesChecker
53 import com.android.wallpaper.module.MultiPanesChecker
54 import com.android.wallpaper.picker.AppbarFragment
55 import com.android.wallpaper.picker.WallpaperPickerDelegate.VIEW_ONLY_PREVIEW_WALLPAPER_REQUEST_CODE
56 import com.android.wallpaper.picker.category.ui.view.CategoriesFragment
57 import com.android.wallpaper.picker.common.preview.data.repository.PersistentWallpaperModelRepository
58 import com.android.wallpaper.picker.common.preview.ui.binder.BasePreviewBinder
59 import com.android.wallpaper.picker.common.preview.ui.binder.PreviewAlphaAnimationBinder
60 import com.android.wallpaper.picker.common.preview.ui.binder.WorkspaceCallbackBinder
61 import com.android.wallpaper.picker.customization.ui.CustomizationPickerActivity2.ActivityEnterAnimationCallback
62 import com.android.wallpaper.picker.customization.ui.binder.ColorUpdateBinder
63 import com.android.wallpaper.picker.customization.ui.binder.CustomizationOptionsBinder
64 import com.android.wallpaper.picker.customization.ui.binder.CustomizationPickerBinder2
65 import com.android.wallpaper.picker.customization.ui.binder.PagerTouchInterceptorBinder
66 import com.android.wallpaper.picker.customization.ui.binder.PreviewLabelBinder
67 import com.android.wallpaper.picker.customization.ui.binder.ToolbarBinder
68 import com.android.wallpaper.picker.customization.ui.util.CustomizationOptionUtil
69 import com.android.wallpaper.picker.customization.ui.util.CustomizationOptionUtil.CustomizationOption
70 import com.android.wallpaper.picker.customization.ui.util.EmptyTransitionListener
71 import com.android.wallpaper.picker.customization.ui.view.WallpaperPickerEntry
72 import com.android.wallpaper.picker.customization.ui.viewmodel.ColorUpdateViewModel
73 import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationPickerViewModel2
74 import com.android.wallpaper.picker.data.WallpaperModel
75 import com.android.wallpaper.picker.di.modules.MainDispatcher
76 import com.android.wallpaper.picker.preview.ui.WallpaperPreviewActivity
77 import com.android.wallpaper.picker.preview.ui.view.ClickableMotionLayout
78 import com.android.wallpaper.util.ActivityUtils
79 import com.android.wallpaper.util.DisplayUtils
80 import com.android.wallpaper.util.WallpaperConnection
81 import com.android.wallpaper.util.wallpaperconnection.WallpaperConnectionUtils
82 import dagger.hilt.android.AndroidEntryPoint
83 import javax.inject.Inject
84 import kotlinx.coroutines.CompletableDeferred
85 import kotlinx.coroutines.CoroutineScope
86 import kotlinx.coroutines.launch
87 
88 @AndroidEntryPoint(AppbarFragment::class)
89 class CustomizationPickerFragment2 :
90     Hilt_CustomizationPickerFragment2(), ActivityEnterAnimationCallback {
91 
92     @Inject lateinit var customizationOptionUtil: CustomizationOptionUtil
93     @Inject lateinit var customizationOptionsBinder: CustomizationOptionsBinder
94     @Inject lateinit var toolbarBinder: ToolbarBinder
95     @Inject lateinit var colorUpdateViewModel: ColorUpdateViewModel
96     @Inject lateinit var clockViewFactory: ClockViewFactory
97     @Inject lateinit var workspaceCallbackBinder: WorkspaceCallbackBinder
98     @Inject lateinit var displayUtils: DisplayUtils
99     @Inject lateinit var wallpaperConnectionUtils: WallpaperConnectionUtils
100     @Inject lateinit var persistentWallpaperModelRepository: PersistentWallpaperModelRepository
101     @Inject @MainDispatcher lateinit var mainScope: CoroutineScope
102     @Inject lateinit var multiPanesChecker: MultiPanesChecker
103 
104     private val customizationPickerViewModel: CustomizationPickerViewModel2 by viewModels()
105 
106     private val isOnMainScreen = {
107         customizationPickerViewModel.customizationOptionsViewModel.selectedOption.value == null
108     }
109 
110     private var fullyCollapsed = false
111 
112     private var onBackPressedCallback: OnBackPressedCallback? = null
113 
114     private val startForResult =
115         this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {}
116 
117     // This boolean is to determine that when onCreateView, if it is a fragment reenter after the
118     // last fragment exit.
119     private var isReenterAfterExit = false
120 
121     private var isInitialCreation = true // Flag to track initial creation
122 
123     override fun onCreate(savedInstanceState: Bundle?) {
124         super.onCreate(savedInstanceState)
125 
126         if (savedInstanceState != null) {
127             // Fragment is being restored, not initial creation
128             isInitialCreation = false
129         }
130 
131         val isFromLauncher =
132             activity?.intent?.let { ActivityUtils.isLaunchedFromLauncher(it) } ?: false
133         if (isFromLauncher) {
134             customizationPickerViewModel.selectPreviewScreen(HOME_SCREEN)
135         }
136         prepareFragmentExitTransitionAnimation()
137         prepareFragmentReenterTransitionAnimation()
138     }
139 
140     override fun onCreateView(
141         inflater: LayoutInflater,
142         container: ViewGroup?,
143         savedInstanceState: Bundle?,
144     ): View {
145         val view = inflater.inflate(R.layout.fragment_customization_picker2, container, false)
146 
147         val toolbar: Toolbar = view.requireViewById(R.id.toolbar) // Toolbar at screen top
148         setupToolbar(
149             view.requireViewById(R.id.nav_button),
150             toolbar,
151             view.requireViewById(R.id.apply_button),
152         )
153 
154         val pickerMotionContainer: MotionLayout = view.requireViewById(R.id.picker_motion_layout)
155         val optionContainer: ConstraintLayout =
156             view.requireViewById(R.id.customization_option_container)
157         val customizationFloatingSheetContainer: FrameLayout =
158             view.requireViewById(R.id.customization_option_floating_sheet_container)
159         ViewCompat.setOnApplyWindowInsetsListener(pickerMotionContainer) { _, windowInsets ->
160             val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
161             val navBarHeight = insets.bottom
162 
163             val horizontalPadding =
164                 resources.getDimensionPixelSize(
165                     R.dimen.customization_option_container_horizontal_padding
166                 )
167             optionContainer.setPaddingRelative(
168                 horizontalPadding,
169                 0,
170                 horizontalPadding,
171                 navBarHeight,
172             )
173 
174             customizationFloatingSheetContainer.setPaddingRelative(0, 0, 0, navBarHeight)
175 
176             val statusBarHeight = insets.top
177             (toolbar.layoutParams as MarginLayoutParams).setMargins(0, statusBarHeight, 0, 0)
178 
179             WindowInsetsCompat.CONSUMED
180         }
181 
182         val customizationOptionFloatingSheetViewMap =
183             customizationOptionUtil.initFloatingSheet(
184                 customizationFloatingSheetContainer,
185                 layoutInflater,
186             )
187 
188         val previewViewModel = customizationPickerViewModel.basePreviewViewModel
189         previewViewModel.setWhichPreview(WallpaperConnection.WhichPreview.PREVIEW_CURRENT)
190         // TODO (b/348462236): adjust flow so this is always false when previewing current wallpaper
191         previewViewModel.setIsWallpaperColorPreviewEnabled(false)
192 
193         val previewPager: ClickableMotionLayout = view.requireViewById(R.id.preview_pager)
194         initPreviewPager(
195             pagerTouchInterceptor = view.requireViewById(R.id.pager_touch_interceptor),
196             clockFaceClickDelegateView = view.requireViewById(R.id.clock_face_click_delegate),
197             previewPager = view.requireViewById(R.id.preview_pager),
198             isFirstBinding = savedInstanceState == null,
199         )
200 
201         if (isInitialCreation) {
202             // If the fragment is created the first time, hide the preview pager. This is to prevent
203             // preview surface views from triggering surfaceCreated too early and binding the
204             // wallpaper and workspace surface. This can potentially block the initiation of the app
205             // start, e.g. Activity's enter animation.
206             // The preview pager will show again when onEnterAnimationCompleteAfterActivityCreated
207             setPreviewPagerVisible(previewPager = previewPager, isVisible = false)
208         }
209 
210         val wallpaperPickerEntry: WallpaperPickerEntry =
211             view.requireViewById(R.id.wallpaper_picker_entry)
212         val previewLabelPlaceHolder: View = view.requireViewById(R.id.label_placeholder)
213         view.post {
214             val wallpaperPickerEntryExpandedHeight = wallpaperPickerEntry.height
215             val wallpaperPickerEntryCollapsedHeight = wallpaperPickerEntry.collapsedButton.height
216             val previewLabelHeight = previewLabelPlaceHolder.height
217             val minCollapsedPreviewHeight =
218                 resources.getDimensionPixelSize(
219                     R.dimen.customization_picker_min_preview_collapsed_height
220                 )
221             val minCollapsedPagerHeight = minCollapsedPreviewHeight + previewLabelHeight
222             val minExpandedPreviewHeight =
223                 resources.getDimensionPixelSize(
224                     R.dimen.customization_picker_min_preview_expanded_height
225                 )
226             val minExpandedPagerHeight = minExpandedPreviewHeight + previewLabelHeight
227 
228             // For collapsed, it needs to show the all option entries, with the collapsed wallpaper
229             // entry, which shows as a single button.
230             val collapsedHeaderHeight =
231                 (pickerMotionContainer.height -
232                         (optionContainer.height -
233                             (wallpaperPickerEntryExpandedHeight -
234                                 wallpaperPickerEntryCollapsedHeight)))
235                     .coerceAtLeast(minCollapsedPagerHeight)
236             pickerMotionContainer
237                 .getConstraintSet(R.id.collapsed_header_primary)
238                 ?.constrainHeight(R.id.preview_header, collapsedHeaderHeight)
239 
240             // The expanded / collapsed header height should be updated when optionContainer
241             // height is known.
242             // For expanded, it needs to show at least half of the entry view below the wallpaper
243             // entry.
244             val expandedHeaderHeight =
245                 (pickerMotionContainer.height -
246                         wallpaperPickerEntryExpandedHeight -
247                         resources.getDimensionPixelSize(R.dimen.customization_option_entry_height) /
248                             2)
249                     .coerceAtLeast(minExpandedPagerHeight)
250             pickerMotionContainer
251                 .getConstraintSet(R.id.expanded_header_primary)
252                 ?.constrainHeight(R.id.preview_header, expandedHeaderHeight)
253 
254             // Transition listener handle 2 things
255             // 1. Expand and collapse the wallpaper entry
256             // 2. Reset the transition and preview when transition back to the primary screen
257             pickerMotionContainer.setTransitionListener(
258                 object : EmptyTransitionListener {
259 
260                     override fun onTransitionChange(
261                         motionLayout: MotionLayout?,
262                         startId: Int,
263                         endId: Int,
264                         progress: Float,
265                     ) {
266                         if (
267                             startId == R.id.expanded_header_primary &&
268                                 endId == R.id.collapsed_header_primary
269                         ) {
270                             wallpaperPickerEntry.setProgress(progress)
271                         }
272                     }
273 
274                     override fun onTransitionCompleted(
275                         motionLayout: MotionLayout?,
276                         currentId: Int,
277                     ) {
278                         if (currentId == R.id.expanded_header_primary) {
279                             wallpaperPickerEntry.setProgress(0f)
280                         } else if (currentId == R.id.collapsed_header_primary) {
281                             wallpaperPickerEntry.setProgress(1f)
282                         }
283 
284                         if (
285                             currentId == R.id.expanded_header_primary ||
286                                 currentId == R.id.collapsed_header_primary
287                         ) {
288                             // This is when we complete the transition back to the primary screen
289                             // Post to let this transition fully complete first
290                             pickerMotionContainer.post {
291                                 pickerMotionContainer.setTransition(R.id.transition_primary)
292                             }
293                             // Reset the preview only after the transition is completed, because the
294                             // reset will trigger the animation of the UI components in the floating
295                             // sheet content, which can possibly be interrupted by the floating
296                             // sheet translating down.
297                             customizationPickerViewModel.customizationOptionsViewModel
298                                 .resetPreview()
299                         }
300                     }
301                 }
302             )
303         }
304 
305         CustomizationPickerBinder2.bind(
306             view = pickerMotionContainer,
307             lockScreenCustomizationOptionEntries =
308                 initCustomizationOptionEntries(view, LOCK_SCREEN),
309             homeScreenCustomizationOptionEntries =
310                 initCustomizationOptionEntries(view, HOME_SCREEN),
311             customizationOptionFloatingSheetViewMap = customizationOptionFloatingSheetViewMap,
312             viewModel = customizationPickerViewModel,
313             colorUpdateViewModel = colorUpdateViewModel,
314             customizationOptionsBinder = customizationOptionsBinder,
315             lifecycleOwner = viewLifecycleOwner,
316             navigateToPrimary = {
317                 if (pickerMotionContainer.currentState == R.id.secondary) {
318                     pickerMotionContainer.transitionToState(
319                         if (fullyCollapsed) R.id.collapsed_header_primary
320                         else R.id.expanded_header_primary
321                     )
322                 }
323             },
324             navigateToSecondary = { screen ->
325                 if (pickerMotionContainer.currentState != R.id.secondary) {
326                     customizationOptionFloatingSheetViewMap[screen]?.let { floatingSheetView ->
327                         setCustomizationOptionFloatingSheet(
328                             floatingSheetViewContent = floatingSheetView,
329                             floatingSheetContainer = customizationFloatingSheetContainer,
330                             motionContainer = pickerMotionContainer,
331                             onComplete = {
332                                 // Transition to secondary screen after content is set
333                                 fullyCollapsed = pickerMotionContainer.progress == 1.0f
334                                 pickerMotionContainer.transitionToState(R.id.secondary)
335                             },
336                         )
337                     }
338                 }
339             },
340             navigateToWallpaperCategoriesScreen = { _ ->
341                 if (isAdded) {
342                     parentFragmentManager.commit {
343                         replace<CategoriesFragment>(R.id.fragment_container)
344                         addToBackStack(null)
345                     }
346                 }
347             },
348             navigateToMoreLockScreenSettingsActivity = {
349                 activity?.startActivity(Intent(Settings.ACTION_LOCKSCREEN_SETTINGS))
350             },
351             navigateToColorContrastSettingsActivity = {
352                 activity?.startActivity(
353                     Intent(Settings.ACTION_ACCESSIBILITY_COLOR_CONTRAST_SETTINGS)
354                 )
355             },
356             navigateToLockScreenNotificationsSettingsActivity = {
357                 activity?.startActivity(Intent(Settings.ACTION_LOCKSCREEN_NOTIFICATIONS_SETTINGS))
358             },
359             navigateToPreviewScreen = { wallpaperModel ->
360                 // navigate to standard preview screen
361                 startWallpaperPreviewActivity(wallpaperModel, false)
362             },
363             navigateToPackThemeActivity = { startPackThemeActivity() },
364         )
365 
366         customizationOptionsBinder.bindDiscardChangesDialog(
367             customizationOptionsViewModel =
368                 customizationPickerViewModel.customizationOptionsViewModel,
369             lifecycleOwner = viewLifecycleOwner,
370             activity = requireActivity(),
371         )
372 
373         activity?.onBackPressedDispatcher?.let {
374             it.addCallback {
375                     isEnabled =
376                         customizationPickerViewModel.customizationOptionsViewModel
377                             .handleBackPressed()
378                     if (!isEnabled) it.onBackPressed()
379                 }
380                 .also { callback -> onBackPressedCallback = callback }
381         }
382 
383         (view as ViewGroup).isTransitionGroup = true
384         return view
385     }
386 
387     override fun onEnterAnimationCompleteAfterActivityCreated() {
388         if (isInitialCreation) {
389             val previewPager: View = view?.findViewById(R.id.preview_pager) ?: return
390             // Show the preview pager only after enter animation completes. If the preview pager was
391             // invisible, making it visible will trigger the surface view's surfaceCreated callback,
392             // as well as the binding of the wallpaper preview and workspace preview.
393             setPreviewPagerVisible(previewPager = previewPager, isVisible = true)
394             isInitialCreation = false
395         }
396     }
397 
398     override fun onDestroyView() {
399         context?.applicationContext?.let { appContext ->
400             // TODO(b/333879532): Only disconnect when leaving the Activity without introducing
401             // black
402             //  preview. If onDestroy is caused by an orientation change, we should keep the
403             // connection
404             //  to avoid initiating the engines again.
405             // TODO(b/328302105): MainScope ensures the job gets done non-blocking even if the
406             //   activity has been destroyed already. Consider making this part of
407             //   WallpaperConnectionUtils.
408             mainScope.launch { wallpaperConnectionUtils.disconnectAll(appContext) }
409         }
410 
411         super.onDestroyView()
412         onBackPressedCallback?.remove()
413     }
414 
415     private fun setupToolbar(navButton: FrameLayout, toolbar: Toolbar, applyButton: Button) {
416         toolbar.title = getString(R.string.app_name)
417         toolbar.setBackgroundColor(Color.TRANSPARENT)
418         toolbarBinder.bind(
419             navButton,
420             toolbar,
421             applyButton,
422             customizationPickerViewModel.customizationOptionsViewModel,
423             colorUpdateViewModel,
424             viewLifecycleOwner,
425         ) {
426             activity?.onBackPressedDispatcher?.onBackPressed()
427         }
428     }
429 
430     private fun initPreviewPager(
431         pagerTouchInterceptor: View,
432         clockFaceClickDelegateView: View,
433         previewPager: ClickableMotionLayout,
434         isFirstBinding: Boolean,
435     ) {
436         PagerTouchInterceptorBinder.bind(
437             pagerTouchInterceptor,
438             customizationPickerViewModel,
439             viewLifecycleOwner,
440         )
441         previewPager.addClickableViewId(R.id.preview_card)
442 
443         val lockPreviewLabel: TextView = previewPager.requireViewById(R.id.lock_preview_label)
444         PreviewLabelBinder.bind(
445             previewLabel = lockPreviewLabel,
446             screen = LOCK_SCREEN,
447             viewModel = customizationPickerViewModel,
448             lifecycleOwner = viewLifecycleOwner,
449         )
450         ColorUpdateBinder.bind(
451             setColor = { color -> lockPreviewLabel.setTextColor(color) },
452             color = colorUpdateViewModel.colorOnSurface,
453             shouldAnimate = isOnMainScreen,
454             lifecycleOwner = viewLifecycleOwner,
455         )
456         val homePreviewLabel: TextView = previewPager.requireViewById(R.id.home_preview_label)
457         PreviewLabelBinder.bind(
458             previewLabel = homePreviewLabel,
459             screen = HOME_SCREEN,
460             viewModel = customizationPickerViewModel,
461             lifecycleOwner = viewLifecycleOwner,
462         )
463         ColorUpdateBinder.bind(
464             setColor = { color -> homePreviewLabel.setTextColor(color) },
465             color = colorUpdateViewModel.colorOnSurface,
466             shouldAnimate = isOnMainScreen,
467             lifecycleOwner = viewLifecycleOwner,
468         )
469 
470         bindPreview(
471             screen = LOCK_SCREEN,
472             clockFaceClickDelegateView = clockFaceClickDelegateView,
473             previewPager = previewPager,
474             preview = previewPager.requireViewById(R.id.lock_preview),
475             isFirstBinding = isFirstBinding,
476         )
477 
478         bindPreview(
479             screen = HOME_SCREEN,
480             clockFaceClickDelegateView = clockFaceClickDelegateView,
481             previewPager = previewPager,
482             preview = previewPager.requireViewById(R.id.home_preview),
483             isFirstBinding = isFirstBinding,
484         )
485 
486         if (isReenterAfterExit) {
487             // If isReenterAfterExit true, it means that it is a fragment reenter after a fragment
488             // exit. Delay PreviewAlphaAnimationBinder.bind() until the reenter onTransitionEnd()
489             // is called.
490             isReenterAfterExit = false
491         } else {
492             // In generally cases, we will bind the animation when onViewCreated()
493             PreviewAlphaAnimationBinder.bind(
494                 previewPager = previewPager,
495                 customizationPickerViewModel,
496                 viewLifecycleOwner,
497             )
498         }
499     }
500 
501     private fun bindPreview(
502         screen: Screen,
503         clockFaceClickDelegateView: View,
504         previewPager: ClickableMotionLayout,
505         preview: View,
506         isFirstBinding: Boolean,
507     ) {
508         val appContext = context?.applicationContext ?: return
509         val activity = activity ?: return
510         val previewViewModel = customizationPickerViewModel.basePreviewViewModel
511 
512         val previewCard: View = preview.requireViewById(R.id.preview_card)
513 
514         if (screen == LOCK_SCREEN) {
515             val clockHostView =
516                 (previewCard.parent as? ViewGroup)?.let {
517                     customizationOptionUtil.createClockPreviewAndAddToParent(it, layoutInflater)
518                 }
519             if (clockHostView != null) {
520                 customizationOptionsBinder.bindClockPreview(
521                     context = requireContext(),
522                     clockHostView = clockHostView,
523                     clockFaceClickDelegateView = clockFaceClickDelegateView,
524                     viewModel = customizationPickerViewModel,
525                     colorUpdateViewModel = colorUpdateViewModel,
526                     lifecycleOwner = viewLifecycleOwner,
527                     clockViewFactory = clockViewFactory,
528                 )
529             }
530         }
531 
532         BasePreviewBinder.bind(
533             applicationContext = appContext,
534             view = previewCard,
535             viewModel = customizationPickerViewModel,
536             colorUpdateViewModel = colorUpdateViewModel,
537             workspaceCallbackBinder = workspaceCallbackBinder,
538             screen = screen,
539             deviceDisplayType = displayUtils.getCurrentDisplayType(activity),
540             displaySize =
541                 if (displayUtils.isOnWallpaperDisplay(activity))
542                     previewViewModel.wallpaperDisplaySize.value
543                 else previewViewModel.smallerDisplaySize,
544             mainScope = mainScope,
545             lifecycleOwner = viewLifecycleOwner,
546             wallpaperConnectionUtils = wallpaperConnectionUtils,
547             isFirstBindingDeferred = CompletableDeferred(isFirstBinding),
548             onLaunchPreview = { wallpaperModel ->
549                 persistentWallpaperModelRepository.setWallpaperModel(wallpaperModel)
550                 val multiPanesChecker = LargeScreenMultiPanesChecker()
551                 val isMultiPanel = multiPanesChecker.isMultiPanesEnabled(appContext)
552                 startForResult.launch(
553                     WallpaperPreviewActivity.newIntent(
554                         context = appContext,
555                         isAssetIdPresent = false,
556                         isViewAsHome = screen == HOME_SCREEN,
557                         isNewTask = isMultiPanel,
558                     )
559                 )
560             },
561             onTransitionToScreen = {
562                 when (it) {
563                     LOCK_SCREEN ->
564                         previewPager.transitionToState(
565                             R.id.lock_preview_selected,
566                             ANIMATION_DURATION,
567                         )
568                     HOME_SCREEN ->
569                         previewPager.transitionToState(
570                             R.id.home_preview_selected,
571                             ANIMATION_DURATION,
572                         )
573                 }
574             },
575             clockViewFactory = clockViewFactory,
576         )
577     }
578 
579     private fun initCustomizationOptionEntries(
580         view: View,
581         screen: Screen,
582     ): List<Pair<CustomizationOption, View>> {
583         val optionEntriesContainer =
584             view.requireViewById<LinearLayout>(
585                 when (screen) {
586                     LOCK_SCREEN -> R.id.lock_customization_option_container
587                     HOME_SCREEN -> R.id.home_customization_option_container
588                 }
589             )
590         val optionEntries =
591             customizationOptionUtil.getOptionEntries(screen, optionEntriesContainer, layoutInflater)
592         optionEntries.onEachIndexed { index, (_, view) ->
593             val isFirst = index == 0
594             val isLast = index == optionEntries.size - 1
595             view.setBackgroundResource(
596                 if (isFirst) R.drawable.customization_option_entry_top_background
597                 else if (isLast) R.drawable.customization_option_entry_bottom_background
598                 else R.drawable.customization_option_entry_background
599             )
600             optionEntriesContainer.addView(view)
601         }
602         return optionEntries
603     }
604 
605     /**
606      * Set customization option floating sheet content to the floating sheet container and get the
607      * new container's height for repositioning the preview's guideline.
608      */
609     private fun setCustomizationOptionFloatingSheet(
610         floatingSheetViewContent: View,
611         floatingSheetContainer: FrameLayout,
612         motionContainer: MotionLayout,
613         onComplete: () -> Unit,
614     ) {
615         floatingSheetContainer.removeAllViews()
616         floatingSheetContainer.addView(floatingSheetViewContent)
617 
618         floatingSheetViewContent.doOnPreDraw {
619             val translationY = floatingSheetViewContent.height
620             floatingSheetContainer.translationY = 0.0f
621             floatingSheetContainer.alpha = 0.0f
622             // Update the motion container
623             motionContainer.getConstraintSet(R.id.expanded_header_primary)?.apply {
624                 setTranslationY(
625                     R.id.customization_option_floating_sheet_container,
626                     translationY.toFloat(),
627                 )
628                 setAlpha(R.id.customization_option_floating_sheet_container, 0.0f)
629                 connect(
630                     R.id.customization_option_floating_sheet_container,
631                     ConstraintSet.BOTTOM,
632                     R.id.picker_motion_layout,
633                     ConstraintSet.BOTTOM,
634                 )
635                 constrainHeight(
636                     R.id.customization_option_floating_sheet_container,
637                     ConstraintLayout.LayoutParams.WRAP_CONTENT,
638                 )
639             }
640             motionContainer.getConstraintSet(R.id.collapsed_header_primary)?.apply {
641                 setTranslationY(
642                     R.id.customization_option_floating_sheet_container,
643                     translationY.toFloat(),
644                 )
645                 setAlpha(R.id.customization_option_floating_sheet_container, 0.0f)
646                 connect(
647                     R.id.customization_option_floating_sheet_container,
648                     ConstraintSet.BOTTOM,
649                     R.id.picker_motion_layout,
650                     ConstraintSet.BOTTOM,
651                 )
652                 constrainHeight(
653                     R.id.customization_option_floating_sheet_container,
654                     ConstraintLayout.LayoutParams.WRAP_CONTENT,
655                 )
656             }
657             motionContainer.getConstraintSet(R.id.secondary)?.apply {
658                 setTranslationY(R.id.customization_option_floating_sheet_container, 0.0f)
659                 setAlpha(R.id.customization_option_floating_sheet_container, 1.0f)
660                 constrainHeight(
661                     R.id.customization_option_floating_sheet_container,
662                     ConstraintLayout.LayoutParams.WRAP_CONTENT,
663                 )
664             }
665             onComplete()
666         }
667     }
668 
669     private fun startWallpaperPreviewActivity(
670         wallpaperModel: WallpaperModel,
671         isCreativeCategories: Boolean,
672     ) {
673         val appContext = requireContext()
674         val activity = requireActivity()
675         persistentWallpaperModelRepository.setWallpaperModel(wallpaperModel)
676         val isMultiPanel = multiPanesChecker.isMultiPanesEnabled(appContext)
677         val previewIntent =
678             WallpaperPreviewActivity.newIntent(
679                 context = appContext,
680                 isAssetIdPresent = true,
681                 isViewAsHome = true,
682                 isNewTask = isMultiPanel,
683                 shouldCategoryRefresh = isCreativeCategories,
684             )
685         ActivityUtils.startActivityForResultSafely(
686             activity,
687             previewIntent,
688             VIEW_ONLY_PREVIEW_WALLPAPER_REQUEST_CODE,
689         )
690     }
691 
692     private fun startPackThemeActivity() {
693         val componentName = ComponentName(PACK_THEME_PACKAGE_NAME, PACK_THEME_SERVICE_NAME)
694         val intent = Intent()
695         intent.setComponent(componentName)
696         startActivity(intent)
697     }
698 
699     companion object {
700         private const val ANIMATION_DURATION = 200
701         private const val PACK_THEME_PACKAGE_NAME =
702             "com.google.android.apps.pixel.customizationbundle"
703         private const val PACK_THEME_SERVICE_NAME =
704             "$PACK_THEME_PACKAGE_NAME.tiktok.app.MainActivity"
705     }
706 
707     private fun prepareFragmentExitTransitionAnimation() {
708         val transition = (exitTransition as? Transition) ?: return
709         transition.addListener(
710             object : Transition.TransitionListener {
711                 override fun onTransitionStart(transition: Transition) {
712                     val previewPager: View = view?.findViewById(R.id.preview_pager) ?: return
713                     setPreviewPagerVisible(previewPager = previewPager, isVisible = false)
714                     isReenterAfterExit = true
715                 }
716 
717                 override fun onTransitionEnd(transition: Transition) {
718                     val previewPager: View = view?.findViewById(R.id.preview_pager) ?: return
719                     setPreviewPagerVisible(previewPager = previewPager, isVisible = true)
720                 }
721 
722                 override fun onTransitionCancel(transition: Transition) {
723                     val previewPager: View = view?.findViewById(R.id.preview_pager) ?: return
724                     setPreviewPagerVisible(previewPager = previewPager, isVisible = true)
725                     isReenterAfterExit = false
726                 }
727 
728                 override fun onTransitionPause(transition: Transition) {}
729 
730                 override fun onTransitionResume(transition: Transition) {}
731             }
732         )
733     }
734 
735     private fun prepareFragmentReenterTransitionAnimation() {
736         val transition = (reenterTransition as? Transition) ?: return
737         transition.addListener(
738             object : Transition.TransitionListener {
739                 override fun onTransitionStart(transition: Transition) {}
740 
741                 override fun onTransitionEnd(transition: Transition) {
742                     val rootView = view ?: return
743                     PreviewAlphaAnimationBinder.bind(
744                         rootView.requireViewById(R.id.preview_pager),
745                         customizationPickerViewModel,
746                         viewLifecycleOwner,
747                     )
748                 }
749 
750                 override fun onTransitionCancel(transition: Transition) {}
751 
752                 override fun onTransitionPause(transition: Transition) {}
753 
754                 override fun onTransitionResume(transition: Transition) {}
755             }
756         )
757     }
758 
759     /**
760      * Specifically set the preview pager visible or invisible. We set the preview pager invisible
761      * early before some Fragment transitions. This is because we encounter the preview flashing
762      * issue due to the unexpected [SurfaceView] callbacks of onSurfaceCreated and
763      * onSurfaceDestroyed, during Fragment transition.
764      */
765     private fun setPreviewPagerVisible(previewPager: View, isVisible: Boolean) {
766         val lockPreview: View = previewPager.requireViewById(R.id.lock_preview)
767         val homePreview: View = previewPager.requireViewById(R.id.home_preview)
768         val lockWallpaperSurface: SurfaceView = lockPreview.requireViewById(R.id.wallpaper_surface)
769         val lockWorkspaceSurface: SurfaceView = lockPreview.requireViewById(R.id.workspace_surface)
770         val homeWallpaperSurface: SurfaceView = homePreview.requireViewById(R.id.wallpaper_surface)
771         val homeWorkspaceSurface: SurfaceView = homePreview.requireViewById(R.id.workspace_surface)
772         lockWallpaperSurface.visibility = if (isVisible) View.VISIBLE else View.INVISIBLE
773         lockWorkspaceSurface.visibility = if (isVisible) View.VISIBLE else View.INVISIBLE
774         homeWallpaperSurface.visibility = if (isVisible) View.VISIBLE else View.INVISIBLE
775         homeWorkspaceSurface.visibility = if (isVisible) View.VISIBLE else View.INVISIBLE
776     }
777 }
778