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