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.binder 17 18 import android.app.AlertDialog 19 import android.app.Flags.liveWallpaperContentHandling 20 import android.content.Intent 21 import android.net.Uri 22 import android.view.View 23 import android.widget.Toast 24 import androidx.activity.OnBackPressedCallback 25 import androidx.constraintlayout.motion.widget.MotionLayout 26 import androidx.core.view.isInvisible 27 import androidx.fragment.app.FragmentActivity 28 import androidx.lifecycle.Lifecycle 29 import androidx.lifecycle.LifecycleOwner 30 import androidx.lifecycle.lifecycleScope 31 import androidx.lifecycle.repeatOnLifecycle 32 import com.android.wallpaper.R 33 import com.android.wallpaper.config.BaseFlags 34 import com.android.wallpaper.model.wallpaper.DeviceDisplayType 35 import com.android.wallpaper.module.logging.UserEventLogger 36 import com.android.wallpaper.picker.preview.ui.util.ImageEffectDialogUtil 37 import com.android.wallpaper.picker.preview.ui.view.ImageEffectDialog 38 import com.android.wallpaper.picker.preview.ui.view.PreviewActionFloatingSheet 39 import com.android.wallpaper.picker.preview.ui.view.PreviewActionGroup 40 import com.android.wallpaper.picker.preview.ui.viewmodel.Action.CUSTOMIZE 41 import com.android.wallpaper.picker.preview.ui.viewmodel.Action.DELETE 42 import com.android.wallpaper.picker.preview.ui.viewmodel.Action.DOWNLOAD 43 import com.android.wallpaper.picker.preview.ui.viewmodel.Action.EDIT 44 import com.android.wallpaper.picker.preview.ui.viewmodel.Action.EFFECTS 45 import com.android.wallpaper.picker.preview.ui.viewmodel.Action.INFORMATION 46 import com.android.wallpaper.picker.preview.ui.viewmodel.Action.SHARE 47 import com.android.wallpaper.picker.preview.ui.viewmodel.PreviewActionsViewModel 48 import com.android.wallpaper.picker.preview.ui.viewmodel.WallpaperPreviewViewModel 49 import com.android.wallpaper.widget.floatingsheetcontent.WallpaperActionsToggleAdapter 50 import com.google.android.material.bottomsheet.BottomSheetBehavior 51 import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN 52 import kotlinx.coroutines.launch 53 54 /** Binds the action buttons and bottom sheet to [PreviewActionsViewModel] */ 55 object PreviewActionsBinder { 56 57 fun bind( 58 actionGroup: PreviewActionGroup, 59 floatingSheet: PreviewActionFloatingSheet, 60 smallPreview: MotionLayout? = null, 61 previewViewModel: WallpaperPreviewViewModel, 62 actionsViewModel: PreviewActionsViewModel, 63 deviceDisplayType: DeviceDisplayType, 64 activity: FragmentActivity, 65 lifecycleOwner: LifecycleOwner, 66 logger: UserEventLogger, 67 imageEffectDialogUtil: ImageEffectDialogUtil, 68 onNavigateToEditScreen: (intent: Intent) -> Unit, 69 onStartShareActivity: (intent: Intent) -> Unit, 70 ) { 71 var deleteDialog: AlertDialog? = null 72 var onDelete: (() -> Unit)? 73 var imageEffectConfirmDownloadDialog: ImageEffectDialog? = null 74 var imageEffectConfirmExitDialog: ImageEffectDialog? = null 75 var onBackPressedCallback: OnBackPressedCallback? = null 76 77 val floatingSheetCallback = 78 object : BottomSheetBehavior.BottomSheetCallback() { 79 override fun onStateChanged(view: View, newState: Int) { 80 // We set visibility to invisible, instead of gone because we listen to the 81 // state change of the BottomSheet and the state change callbacks are only fired 82 // when the view is not gone. 83 if (newState == STATE_HIDDEN) { 84 actionsViewModel.onFloatingSheetCollapsed() 85 if (BaseFlags.get().isNewPickerUi()) 86 smallPreview?.transitionToState(R.id.floating_sheet_gone) 87 else floatingSheet.isInvisible = true 88 } else { 89 if (BaseFlags.get().isNewPickerUi()) 90 smallPreview?.transitionToState(R.id.floating_sheet_visible) 91 else floatingSheet.isInvisible = false 92 } 93 } 94 95 override fun onSlide(p0: View, p1: Float) {} 96 } 97 val noActionChecked = !actionsViewModel.isAnyActionChecked() 98 if (BaseFlags.get().isNewPickerUi()) { 99 if (noActionChecked) { 100 smallPreview?.transitionToState(R.id.floating_sheet_gone) 101 } else { 102 smallPreview?.transitionToState(R.id.floating_sheet_visible) 103 } 104 } else { 105 floatingSheet.isInvisible = noActionChecked 106 } 107 floatingSheet.addFloatingSheetCallback(floatingSheetCallback) 108 lifecycleOwner.lifecycleScope.launch { 109 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { 110 floatingSheet.addFloatingSheetCallback(floatingSheetCallback) 111 } 112 floatingSheet.removeFloatingSheetCallback(floatingSheetCallback) 113 } 114 115 lifecycleOwner.lifecycleScope.launch { 116 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 117 /** [INFORMATION] */ 118 launch { 119 actionsViewModel.isInformationVisible.collect { 120 actionGroup.setIsVisible(INFORMATION, it) 121 } 122 } 123 124 launch { 125 actionsViewModel.isInformationChecked.collect { 126 actionGroup.setIsChecked(INFORMATION, it) 127 } 128 } 129 130 launch { 131 actionsViewModel.onInformationClicked.collect { 132 actionGroup.setClickListener(INFORMATION, it) 133 } 134 } 135 136 /** [DOWNLOAD] */ 137 launch { 138 actionsViewModel.isDownloadVisible.collect { 139 actionGroup.setIsVisible(DOWNLOAD, it) 140 } 141 } 142 143 launch { 144 actionsViewModel.isDownloading.collect { actionGroup.setIsDownloading(it) } 145 } 146 147 launch { 148 actionsViewModel.isDownloadButtonEnabled.collect { 149 actionGroup.setClickListener( 150 DOWNLOAD, 151 if (it) { 152 { actionsViewModel.downloadWallpaper() } 153 } else null, 154 ) 155 } 156 } 157 158 /** [DELETE] */ 159 launch { 160 actionsViewModel.isDeleteVisible.collect { 161 actionGroup.setIsVisible(DELETE, it) 162 } 163 } 164 165 launch { 166 actionsViewModel.isDeleteChecked.collect { 167 actionGroup.setIsChecked(DELETE, it) 168 } 169 } 170 171 launch { 172 actionsViewModel.onDeleteClicked.collect { 173 actionGroup.setClickListener(DELETE, it) 174 } 175 } 176 177 launch { 178 actionsViewModel.deleteConfirmationDialogViewModel.collect { viewModel -> 179 val appContext = activity.applicationContext 180 if (viewModel != null) { 181 onDelete = { 182 if (viewModel.creativeWallpaperDeleteUri != null) { 183 appContext.contentResolver.delete( 184 viewModel.creativeWallpaperDeleteUri, 185 null, 186 null, 187 ) 188 } else if (viewModel.liveWallpaperDeleteIntent != null) { 189 appContext.startService(viewModel.liveWallpaperDeleteIntent) 190 } 191 activity.finish() 192 } 193 val dialog = 194 deleteDialog 195 ?: AlertDialog.Builder(activity) 196 .setMessage(R.string.delete_wallpaper_confirmation) 197 .setOnDismissListener { viewModel.onDismiss.invoke() } 198 .setPositiveButton(R.string.delete_live_wallpaper) { _, _ -> 199 onDelete?.invoke() 200 } 201 .setNegativeButton(android.R.string.cancel, null) 202 .create() 203 .also { deleteDialog = it } 204 dialog.show() 205 } else { 206 deleteDialog?.dismiss() 207 } 208 } 209 } 210 211 /** [EDIT] */ 212 launch { 213 actionsViewModel.isEditVisible.collect { actionGroup.setIsVisible(EDIT, it) } 214 } 215 216 launch { 217 actionsViewModel.isEditChecked.collect { actionGroup.setIsChecked(EDIT, it) } 218 } 219 220 launch { 221 actionsViewModel.editIntent.collect { 222 actionGroup.setClickListener( 223 EDIT, 224 if (it != null) { 225 { 226 // We need to set default wallpaper preview config view model 227 // before entering full screen with edit activity overlay. 228 previewViewModel.setDefaultFullPreviewConfigViewModel( 229 deviceDisplayType 230 ) 231 onNavigateToEditScreen.invoke(it) 232 } 233 } else null, 234 ) 235 } 236 } 237 238 /** [CUSTOMIZE] */ 239 launch { 240 actionsViewModel.isCustomizeVisible.collect { 241 actionGroup.setIsVisible(CUSTOMIZE, it) 242 } 243 } 244 245 launch { 246 actionsViewModel.isCustomizeChecked.collect { 247 actionGroup.setIsChecked(CUSTOMIZE, it) 248 } 249 } 250 251 launch { 252 actionsViewModel.onCustomizeClicked.collect { 253 actionGroup.setClickListener(CUSTOMIZE, it) 254 } 255 } 256 257 /** [EFFECTS] */ 258 launch { 259 actionsViewModel.isEffectsVisible.collect { 260 actionGroup.setIsVisible(EFFECTS, it) 261 } 262 } 263 264 launch { 265 actionsViewModel.isEffectsChecked.collect { 266 actionGroup.setIsChecked(EFFECTS, it) 267 } 268 } 269 270 launch { 271 actionsViewModel.onEffectsClicked.collect { 272 actionGroup.setClickListener(EFFECTS, it) 273 } 274 } 275 276 launch { 277 actionsViewModel.effectDownloadFailureToastText.collect { 278 Toast.makeText(floatingSheet.context, it, Toast.LENGTH_LONG).show() 279 } 280 } 281 282 launch { 283 actionsViewModel.imageEffectConfirmDownloadDialogViewModel.collect { viewModel 284 -> 285 if (viewModel != null) { 286 val dialog = 287 imageEffectConfirmDownloadDialog 288 ?: imageEffectDialogUtil 289 .createConfirmDownloadDialog(activity) 290 .also { imageEffectConfirmDownloadDialog = it } 291 dialog.onDismiss = viewModel.onDismiss 292 dialog.onContinue = viewModel.onContinue 293 dialog.show() 294 } else { 295 imageEffectConfirmDownloadDialog?.dismiss() 296 } 297 } 298 } 299 300 launch { 301 actionsViewModel.imageEffectConfirmExitDialogViewModel.collect { viewModel -> 302 if (viewModel != null) { 303 val dialog = 304 imageEffectConfirmExitDialog 305 ?: imageEffectDialogUtil 306 .createConfirmExitDialog(activity) 307 .also { imageEffectConfirmExitDialog = it } 308 dialog.onDismiss = viewModel.onDismiss 309 dialog.onContinue = { 310 viewModel.onContinue() 311 activity.onBackPressedDispatcher.onBackPressed() 312 } 313 dialog.show() 314 } else { 315 imageEffectConfirmExitDialog?.dismiss() 316 } 317 } 318 } 319 320 launch { 321 actionsViewModel.handleOnBackPressed.collect { handleOnBackPressed -> 322 // Reset the callback 323 onBackPressedCallback?.remove() 324 onBackPressedCallback = null 325 if (handleOnBackPressed != null) { 326 // If handleOnBackPressed is not null, set it to the activity 327 val callback = 328 object : OnBackPressedCallback(true) { 329 override fun handleOnBackPressed() { 330 val handled = handleOnBackPressed() 331 if (!handled) { 332 onBackPressedCallback?.remove() 333 onBackPressedCallback = null 334 activity.onBackPressedDispatcher.onBackPressed() 335 } 336 } 337 } 338 .also { onBackPressedCallback = it } 339 activity.onBackPressedDispatcher.addCallback(lifecycleOwner, callback) 340 } 341 } 342 } 343 344 /** [SHARE] */ 345 launch { 346 actionsViewModel.isShareVisible.collect { actionGroup.setIsVisible(SHARE, it) } 347 } 348 349 launch { 350 actionsViewModel.shareIntent.collect { 351 actionGroup.setClickListener( 352 SHARE, 353 if (it != null) { 354 { onStartShareActivity.invoke(it) } 355 } else null, 356 ) 357 } 358 } 359 360 /** Floating sheet behavior */ 361 launch { 362 actionsViewModel.previewFloatingSheetViewModel.collect { floatingSheetViewModel 363 -> 364 if (floatingSheetViewModel != null) { 365 val ( 366 informationViewModel, 367 imageEffectViewModel, 368 creativeEffectViewModel, 369 customizeViewModel, 370 ) = floatingSheetViewModel 371 when { 372 informationViewModel != null -> { 373 if (liveWallpaperContentHandling()) { 374 floatingSheet.setInformationContent( 375 description = informationViewModel.description, 376 attributions = informationViewModel.attributions, 377 onExploreButtonClickListener = 378 (informationViewModel.description?.contextUri 379 ?: informationViewModel.actionUrl?.let { 380 Uri.parse(it) 381 }) 382 ?.let { uri -> 383 { 384 logger 385 .logWallpaperExploreButtonClicked() 386 floatingSheet.context.startActivity( 387 Intent(Intent.ACTION_VIEW, uri) 388 ) 389 } 390 }, 391 actionButtonTitle = 392 informationViewModel.description?.contextDescription 393 ?: informationViewModel.actionButtonTitle, 394 ) 395 } else { 396 floatingSheet.setInformationContent( 397 description = null, 398 attributions = informationViewModel.attributions, 399 onExploreButtonClickListener = 400 informationViewModel.actionUrl?.let { url -> 401 { 402 logger.logWallpaperExploreButtonClicked() 403 floatingSheet.context.startActivity( 404 Intent( 405 Intent.ACTION_VIEW, 406 Uri.parse(url), 407 ) 408 ) 409 } 410 }, 411 actionButtonTitle = 412 informationViewModel.actionButtonTitle, 413 ) 414 } 415 } 416 imageEffectViewModel != null -> 417 floatingSheet.setImageEffectContent( 418 imageEffectViewModel.effectType, 419 imageEffectViewModel.myPhotosClickListener, 420 imageEffectViewModel.collapseFloatingSheetListener, 421 imageEffectViewModel.effectSwitchListener, 422 imageEffectViewModel.effectDownloadClickListener, 423 imageEffectViewModel.status, 424 imageEffectViewModel.resultCode, 425 imageEffectViewModel.errorMessage, 426 imageEffectViewModel.title, 427 imageEffectViewModel.effectTextRes, 428 ) 429 creativeEffectViewModel != null -> 430 floatingSheet.setCreativeEffectContent( 431 creativeEffectViewModel.title, 432 creativeEffectViewModel.subtitle, 433 creativeEffectViewModel.wallpaperActions, 434 object : 435 WallpaperActionsToggleAdapter.WallpaperEffectSwitchListener { 436 override fun onEffectSwitchChanged(checkedItem: Int) { 437 launch { 438 creativeEffectViewModel 439 .wallpaperEffectSwitchListener(checkedItem) 440 } 441 } 442 }, 443 ) 444 customizeViewModel != null -> 445 floatingSheet.setCustomizeContent( 446 customizeViewModel.customizeSliceUri 447 ) 448 } 449 floatingSheet.expand() 450 } else { 451 floatingSheet.collapse() 452 } 453 } 454 } 455 } 456 } 457 } 458 459 private fun getActionUri(actionUrl: String?, contextUri: Uri?): Uri? { 460 val actionUri = actionUrl?.let { Uri.parse(actionUrl) } 461 return contextUri ?: actionUri 462 } 463 } 464