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