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 
17 package com.android.wallpaper.picker.preview.ui.viewmodel
18 
19 import android.content.ClipData
20 import android.content.ComponentName
21 import android.content.Context
22 import android.content.Intent
23 import android.net.ConnectivityManager
24 import android.net.Uri
25 import android.net.wifi.WifiManager
26 import android.service.wallpaper.WallpaperSettingsActivity
27 import com.android.wallpaper.effects.Effect
28 import com.android.wallpaper.effects.EffectsController.EffectEnumInterface
29 import com.android.wallpaper.picker.data.CreativeWallpaperData
30 import com.android.wallpaper.picker.data.DownloadableWallpaperData
31 import com.android.wallpaper.picker.data.LiveWallpaperData
32 import com.android.wallpaper.picker.data.WallpaperModel
33 import com.android.wallpaper.picker.data.WallpaperModel.LiveWallpaperModel
34 import com.android.wallpaper.picker.preview.data.repository.ImageEffectsRepository.EffectStatus.EFFECT_APPLIED
35 import com.android.wallpaper.picker.preview.data.repository.ImageEffectsRepository.EffectStatus.EFFECT_APPLY_FAILED
36 import com.android.wallpaper.picker.preview.data.repository.ImageEffectsRepository.EffectStatus.EFFECT_APPLY_IN_PROGRESS
37 import com.android.wallpaper.picker.preview.data.repository.ImageEffectsRepository.EffectStatus.EFFECT_DISABLE
38 import com.android.wallpaper.picker.preview.data.repository.ImageEffectsRepository.EffectStatus.EFFECT_DOWNLOAD_FAILED
39 import com.android.wallpaper.picker.preview.data.repository.ImageEffectsRepository.EffectStatus.EFFECT_DOWNLOAD_IN_PROGRESS
40 import com.android.wallpaper.picker.preview.data.repository.ImageEffectsRepository.EffectStatus.EFFECT_DOWNLOAD_READY
41 import com.android.wallpaper.picker.preview.data.repository.ImageEffectsRepository.EffectStatus.EFFECT_READY
42 import com.android.wallpaper.picker.preview.domain.interactor.PreviewActionsInteractor
43 import com.android.wallpaper.picker.preview.shared.model.ImageEffectsModel
44 import com.android.wallpaper.picker.preview.ui.util.LiveWallpaperDeleteUtil
45 import com.android.wallpaper.picker.preview.ui.viewmodel.Action.CUSTOMIZE
46 import com.android.wallpaper.picker.preview.ui.viewmodel.Action.DELETE
47 import com.android.wallpaper.picker.preview.ui.viewmodel.Action.EDIT
48 import com.android.wallpaper.picker.preview.ui.viewmodel.Action.EFFECTS
49 import com.android.wallpaper.picker.preview.ui.viewmodel.Action.INFORMATION
50 import com.android.wallpaper.picker.preview.ui.viewmodel.Action.SHARE
51 import com.android.wallpaper.picker.preview.ui.viewmodel.floatingSheet.CreativeEffectFloatingSheetViewModel
52 import com.android.wallpaper.picker.preview.ui.viewmodel.floatingSheet.CustomizeFloatingSheetViewModel
53 import com.android.wallpaper.picker.preview.ui.viewmodel.floatingSheet.ImageEffectFloatingSheetViewModel
54 import com.android.wallpaper.picker.preview.ui.viewmodel.floatingSheet.InformationFloatingSheetViewModel
55 import com.android.wallpaper.picker.preview.ui.viewmodel.floatingSheet.PreviewFloatingSheetViewModel
56 import com.android.wallpaper.widget.floatingsheetcontent.WallpaperEffectsView2.EffectDownloadClickListener
57 import com.android.wallpaper.widget.floatingsheetcontent.WallpaperEffectsView2.EffectSwitchListener
58 import com.android.wallpaper.widget.floatingsheetcontent.WallpaperEffectsView2.Status.DOWNLOADING
59 import com.android.wallpaper.widget.floatingsheetcontent.WallpaperEffectsView2.Status.FAILED
60 import com.android.wallpaper.widget.floatingsheetcontent.WallpaperEffectsView2.Status.IDLE
61 import com.android.wallpaper.widget.floatingsheetcontent.WallpaperEffectsView2.Status.PROCESSING
62 import com.android.wallpaper.widget.floatingsheetcontent.WallpaperEffectsView2.Status.SHOW_DOWNLOAD_BUTTON
63 import com.android.wallpaper.widget.floatingsheetcontent.WallpaperEffectsView2.Status.SUCCESS
64 import dagger.hilt.android.qualifiers.ApplicationContext
65 import dagger.hilt.android.scopes.ViewModelScoped
66 import javax.inject.Inject
67 import kotlinx.coroutines.flow.Flow
68 import kotlinx.coroutines.flow.MutableStateFlow
69 import kotlinx.coroutines.flow.asStateFlow
70 import kotlinx.coroutines.flow.combine
71 import kotlinx.coroutines.flow.filterNotNull
72 import kotlinx.coroutines.flow.map
73 
74 /** View model for the preview action buttons */
75 @ViewModelScoped
76 class PreviewActionsViewModel
77 @Inject
78 constructor(
79     private val interactor: PreviewActionsInteractor,
80     liveWallpaperDeleteUtil: LiveWallpaperDeleteUtil,
81     @ApplicationContext private val context: Context,
82 ) {
83     /** [INFORMATION] */
84     private val informationFloatingSheetViewModel: Flow<InformationFloatingSheetViewModel?> =
85         interactor.wallpaperModel.map { wallpaperModel ->
86             if (wallpaperModel == null || !wallpaperModel.shouldShowInformationFloatingSheet()) {
87                 null
88             } else {
89                 InformationFloatingSheetViewModel(
90                     wallpaperModel.commonWallpaperData.attributions,
91                     if (wallpaperModel.commonWallpaperData.exploreActionUrl.isNullOrEmpty()) {
92                         null
93                     } else {
94                         wallpaperModel.commonWallpaperData.exploreActionUrl
95                     }
96                 )
97             }
98         }
99 
100     val isInformationVisible: Flow<Boolean> = informationFloatingSheetViewModel.map { it != null }
101 
102     private val _isInformationChecked: MutableStateFlow<Boolean> = MutableStateFlow(false)
103     val isInformationChecked: Flow<Boolean> = _isInformationChecked.asStateFlow()
104 
105     val onInformationClicked: Flow<(() -> Unit)?> =
106         combine(isInformationVisible, isInformationChecked) { show, isChecked ->
107             if (show) {
108                 {
109                     if (!isChecked) {
110                         uncheckAllOthersExcept(INFORMATION)
111                     }
112                     _isInformationChecked.value = !isChecked
113                 }
114             } else {
115                 null
116             }
117         }
118 
119     /** [DOWNLOAD] */
120     private val downloadableWallpaperData: Flow<DownloadableWallpaperData?> =
121         interactor.wallpaperModel.map {
122             (it as? WallpaperModel.StaticWallpaperModel)?.downloadableWallpaperData
123         }
124     val isDownloadVisible: Flow<Boolean> = downloadableWallpaperData.map { it != null }
125 
126     val isDownloading: Flow<Boolean> = interactor.isDownloadingWallpaper
127 
128     val isDownloadButtonEnabled: Flow<Boolean> =
129         combine(downloadableWallpaperData, isDownloading) { downloadableData, isDownloading ->
130             downloadableData != null && !isDownloading
131         }
132 
133     suspend fun downloadWallpaper() {
134         interactor.downloadWallpaper()
135     }
136 
137     /** [DELETE] */
138     private val liveWallpaperDeleteIntent: Flow<Intent?> =
139         interactor.wallpaperModel.map {
140             if (it is LiveWallpaperModel && it.creativeWallpaperData == null && it.canBeDeleted()) {
141                 liveWallpaperDeleteUtil.getDeleteActionIntent(
142                     it.liveWallpaperData.systemWallpaperInfo
143                 )
144             } else {
145                 null
146             }
147         }
148     private val creativeWallpaperDeleteUri: Flow<Uri?> =
149         interactor.wallpaperModel.map {
150             val deleteUri = (it as? LiveWallpaperModel)?.creativeWallpaperData?.deleteUri
151             if (deleteUri != null && it.canBeDeleted()) {
152                 deleteUri
153             } else {
154                 null
155             }
156         }
157     val isDeleteVisible: Flow<Boolean> =
158         combine(liveWallpaperDeleteIntent, creativeWallpaperDeleteUri) { intent, uri ->
159             intent != null || uri != null
160         }
161 
162     private val _isDeleteChecked: MutableStateFlow<Boolean> = MutableStateFlow(false)
163     val isDeleteChecked: Flow<Boolean> = _isDeleteChecked.asStateFlow()
164 
165     // View model for delete confirmation dialog. Note that null means the dialog should show;
166     // otherwise, the dialog should hide.
167     val deleteConfirmationDialogViewModel: Flow<DeleteConfirmationDialogViewModel?> =
168         combine(isDeleteChecked, liveWallpaperDeleteIntent, creativeWallpaperDeleteUri) {
169             isChecked,
170             intent,
171             uri ->
172             if (isChecked && (intent != null || uri != null)) {
173                 DeleteConfirmationDialogViewModel(
174                     onDismiss = { _isDeleteChecked.value = false },
175                     liveWallpaperDeleteIntent = intent,
176                     creativeWallpaperDeleteUri = uri,
177                 )
178             } else {
179                 null
180             }
181         }
182 
183     val onDeleteClicked: Flow<(() -> Unit)?> =
184         combine(isDeleteVisible, isDeleteChecked) { show, isChecked ->
185             if (show) {
186                 {
187                     if (!isChecked) {
188                         uncheckAllOthersExcept(DELETE)
189                     }
190                     _isDeleteChecked.value = !isChecked
191                 }
192             } else {
193                 null
194             }
195         }
196 
197     /** [EDIT] */
198     val editIntent: Flow<Intent?> =
199         interactor.wallpaperModel.map { model ->
200             (model as? LiveWallpaperModel)?.liveWallpaperData?.getEditActivityIntent(false)?.let {
201                 intent ->
202                 if (intent.resolveActivityInfo(context.packageManager, 0) != null) intent else null
203             }
204         }
205     val isEditVisible: Flow<Boolean> = editIntent.map { it != null }
206 
207     private val _isEditChecked: MutableStateFlow<Boolean> = MutableStateFlow(false)
208     val isEditChecked: Flow<Boolean> = _isEditChecked.asStateFlow()
209 
210     /** [CUSTOMIZE] */
211     private val customizeFloatingSheetViewModel: Flow<CustomizeFloatingSheetViewModel?> =
212         interactor.wallpaperModel.map {
213             (it as? LiveWallpaperModel)
214                 ?.liveWallpaperData
215                 ?.systemWallpaperInfo
216                 ?.settingsSliceUri
217                 ?.let { CustomizeFloatingSheetViewModel(it) }
218         }
219     val isCustomizeVisible: Flow<Boolean> = customizeFloatingSheetViewModel.map { it != null }
220 
221     private val _isCustomizeChecked: MutableStateFlow<Boolean> = MutableStateFlow(false)
222     val isCustomizeChecked: Flow<Boolean> = _isCustomizeChecked.asStateFlow()
223 
224     val onCustomizeClicked: Flow<(() -> Unit)?> =
225         combine(isCustomizeVisible, isCustomizeChecked) { show, isChecked ->
226             if (show) {
227                 {
228                     if (!isChecked) {
229                         uncheckAllOthersExcept(CUSTOMIZE)
230                     }
231                     _isCustomizeChecked.value = !isChecked
232                 }
233             } else {
234                 null
235             }
236         }
237 
238     /** [EFFECTS] */
239     private val _imageEffectConfirmDownloadDialogViewModel:
240         MutableStateFlow<ImageEffectDialogViewModel?> =
241         MutableStateFlow(null)
242     // View model for the dialog that confirms downloading the effect ML model.
243     val imageEffectConfirmDownloadDialogViewModel =
244         _imageEffectConfirmDownloadDialogViewModel.asStateFlow()
245 
246     private val imageEffectFloatingSheetViewModel: Flow<ImageEffectFloatingSheetViewModel?> =
247         combine(interactor.imageEffectsModel, interactor.imageEffect) {
248             imageEffectsModel,
249             imageEffect ->
250             imageEffect?.let {
251                 when (imageEffectsModel.status) {
252                     EFFECT_DISABLE -> {
253                         null
254                     }
255                     else -> {
256                         getImageEffectFloatingSheetViewModel(
257                             imageEffect,
258                             imageEffectsModel,
259                         )
260                     }
261                 }
262             }
263         }
264     private val _imageEffectConfirmExitDialogViewModel:
265         MutableStateFlow<ImageEffectDialogViewModel?> =
266         MutableStateFlow(null)
267     val imageEffectConfirmExitDialogViewModel = _imageEffectConfirmExitDialogViewModel.asStateFlow()
268     val handleOnBackPressed: Flow<(() -> Boolean)?> =
269         combine(imageEffectFloatingSheetViewModel, interactor.imageEffect, isDownloading) {
270             viewModel,
271             effect,
272             isDownloading ->
273             when {
274                 viewModel?.status == DOWNLOADING -> { ->
275                         _imageEffectConfirmExitDialogViewModel.value =
276                             ImageEffectDialogViewModel(
277                                 onDismiss = { _imageEffectConfirmExitDialogViewModel.value = null },
278                                 onContinue = {
279                                     // Continue to exit the screen. We should stop downloading.
280                                     effect?.let { interactor.interruptEffectsModelDownload(it) }
281                                 },
282                             )
283                         true
284                     }
285                 isDownloading -> { -> interactor.cancelDownloadWallpaper() }
286                 else -> null
287             }
288         }
289 
290     private val creativeEffectFloatingSheetViewModel: Flow<CreativeEffectFloatingSheetViewModel?> =
291         interactor.creativeEffectsModel.map { creativeEffectsModel ->
292             creativeEffectsModel?.let {
293                 CreativeEffectFloatingSheetViewModel(
294                     title = it.title,
295                     subtitle = it.subtitle,
296                     wallpaperActions = it.actions,
297                     wallpaperEffectSwitchListener = { interactor.turnOnCreativeEffect(it) },
298                 )
299             }
300         }
301 
302     private fun getImageEffectFloatingSheetViewModel(
303         effect: Effect,
304         imageEffectsModel: ImageEffectsModel,
305     ): ImageEffectFloatingSheetViewModel {
306         val floatingSheetViewStatus =
307             when (imageEffectsModel.status) {
308                 EFFECT_DISABLE -> {
309                     FAILED
310                 }
311                 EFFECT_READY -> {
312                     IDLE
313                 }
314                 EFFECT_DOWNLOAD_READY -> {
315                     SHOW_DOWNLOAD_BUTTON
316                 }
317                 EFFECT_DOWNLOAD_IN_PROGRESS -> {
318                     DOWNLOADING
319                 }
320                 EFFECT_APPLY_IN_PROGRESS -> {
321                     PROCESSING
322                 }
323                 EFFECT_APPLIED -> {
324                     SUCCESS
325                 }
326                 EFFECT_DOWNLOAD_FAILED -> {
327                     SHOW_DOWNLOAD_BUTTON
328                 }
329                 EFFECT_APPLY_FAILED -> {
330                     FAILED
331                 }
332             }
333         return ImageEffectFloatingSheetViewModel(
334             myPhotosClickListener = {},
335             collapseFloatingSheetListener = {},
336             object : EffectSwitchListener {
337                 override fun onEffectSwitchChanged(
338                     effect: EffectEnumInterface,
339                     isChecked: Boolean
340                 ) {
341                     if (interactor.isTargetEffect(effect)) {
342                         if (isChecked) {
343                             interactor.enableImageEffect(effect)
344                         } else {
345                             interactor.disableImageEffect()
346                         }
347                     }
348                 }
349             },
350             object : EffectDownloadClickListener {
351                 override fun onEffectDownloadClick() {
352                     if (isWifiOnAndConnected()) {
353                         interactor.startEffectsModelDownload(effect)
354                     } else {
355                         _imageEffectConfirmDownloadDialogViewModel.value =
356                             ImageEffectDialogViewModel(
357                                 onDismiss = {
358                                     _imageEffectConfirmDownloadDialogViewModel.value = null
359                                 },
360                                 onContinue = {
361                                     // Continue to download the ML model
362                                     interactor.startEffectsModelDownload(effect)
363                                 },
364                             )
365                     }
366                 }
367             },
368             floatingSheetViewStatus,
369             imageEffectsModel.resultCode,
370             imageEffectsModel.errorMessage,
371             effect.title,
372             effect.type,
373             interactor.getEffectTextRes(),
374         )
375     }
376 
377     private fun isWifiOnAndConnected(): Boolean {
378         val wifiMgr = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
379         return if (wifiMgr.isWifiEnabled) { // Wi-Fi adapter is ON
380             val connMgr =
381                 context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
382             val wifiInfo = wifiMgr.connectionInfo
383             val wifi = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
384             val signalLevel = wifiMgr.calculateSignalLevel(wifiInfo.rssi)
385             signalLevel > 0 && wifi!!.isConnectedOrConnecting
386         } else {
387             false
388         }
389     }
390 
391     val isEffectsVisible: Flow<Boolean> =
392         combine(imageEffectFloatingSheetViewModel, creativeEffectFloatingSheetViewModel) {
393             imageEffect,
394             creativeEffect ->
395             imageEffect != null || creativeEffect != null
396         }
397 
398     private val _isEffectsChecked: MutableStateFlow<Boolean> = MutableStateFlow(false)
399     val isEffectsChecked: Flow<Boolean> = _isEffectsChecked.asStateFlow()
400 
401     val onEffectsClicked: Flow<(() -> Unit)?> =
402         combine(isEffectsVisible, isEffectsChecked) { show, isChecked ->
403             if (show) {
404                 {
405                     if (!isChecked) {
406                         uncheckAllOthersExcept(EFFECTS)
407                     }
408                     _isEffectsChecked.value = !isChecked
409                 }
410             } else {
411                 null
412             }
413         }
414 
415     val effectDownloadFailureToastText: Flow<String> =
416         interactor.imageEffectsModel
417             .map { if (it.status == EFFECT_DOWNLOAD_FAILED) it.errorMessage else null }
418             .filterNotNull()
419 
420     /** [SHARE] */
421     val shareIntent: Flow<Intent?> =
422         interactor.wallpaperModel.map { model ->
423             (model as? LiveWallpaperModel)?.creativeWallpaperData?.let { data ->
424                 if (data.shareUri == null || data.shareUri == Uri.EMPTY) null
425                 else data.getShareIntent()
426             }
427         }
428     val isShareVisible: Flow<Boolean> = shareIntent.map { it != null }
429 
430     // Floating sheet contents for the bottom sheet dialog. If content is null, the bottom sheet
431     // should collapse, otherwise, expended.
432     val previewFloatingSheetViewModel: Flow<PreviewFloatingSheetViewModel?> =
433         combine7(
434             isInformationChecked,
435             isEffectsChecked,
436             isCustomizeChecked,
437             informationFloatingSheetViewModel,
438             imageEffectFloatingSheetViewModel,
439             creativeEffectFloatingSheetViewModel,
440             customizeFloatingSheetViewModel,
441         ) {
442             isInformationChecked,
443             isEffectsChecked,
444             isCustomizeChecked,
445             informationFloatingSheetViewModel,
446             imageEffectFloatingSheetViewModel,
447             creativeEffectFloatingSheetViewModel,
448             customizeFloatingSheetViewModel ->
449             if (isInformationChecked && informationFloatingSheetViewModel != null) {
450                 PreviewFloatingSheetViewModel(
451                     informationFloatingSheetViewModel = informationFloatingSheetViewModel
452                 )
453             } else if (isEffectsChecked && imageEffectFloatingSheetViewModel != null) {
454                 PreviewFloatingSheetViewModel(
455                     imageEffectFloatingSheetViewModel = imageEffectFloatingSheetViewModel
456                 )
457             } else if (isEffectsChecked && creativeEffectFloatingSheetViewModel != null) {
458                 PreviewFloatingSheetViewModel(
459                     creativeEffectFloatingSheetViewModel = creativeEffectFloatingSheetViewModel
460                 )
461             } else if (isCustomizeChecked && customizeFloatingSheetViewModel != null) {
462                 PreviewFloatingSheetViewModel(
463                     customizeFloatingSheetViewModel = customizeFloatingSheetViewModel
464                 )
465             } else {
466                 null
467             }
468         }
469 
470     fun onFloatingSheetCollapsed() {
471         // When floating collapsed, we should look for those actions that expand the floating sheet
472         // and see which is checked, and uncheck it.
473         if (_isInformationChecked.value) {
474             _isInformationChecked.value = false
475         }
476 
477         if (_isEffectsChecked.value) {
478             _isEffectsChecked.value = false
479         }
480 
481         if (_isCustomizeChecked.value) {
482             _isCustomizeChecked.value = false
483         }
484     }
485 
486     private fun uncheckAllOthersExcept(action: Action) {
487         if (action != INFORMATION) {
488             _isInformationChecked.value = false
489         }
490         if (action != DELETE) {
491             _isDeleteChecked.value = false
492         }
493         if (action != EDIT) {
494             _isEditChecked.value = false
495         }
496         if (action != CUSTOMIZE) {
497             _isCustomizeChecked.value = false
498         }
499         if (action != EFFECTS) {
500             _isEffectsChecked.value = false
501         }
502     }
503 
504     companion object {
505         const val EXTRA_KEY_IS_CREATE_NEW = "is_create_new"
506 
507         private fun WallpaperModel.shouldShowInformationFloatingSheet(): Boolean {
508             if (
509                 this is LiveWallpaperModel &&
510                     !liveWallpaperData.systemWallpaperInfo.showMetadataInPreview
511             ) {
512                 // If the live wallpaper's flag of showMetadataInPreview is false, do not show the
513                 // information floating sheet.
514                 return false
515             }
516             val attributions = commonWallpaperData.attributions
517             // Show information floating sheet when any of the following contents exists
518             // 1. Attributions: Any of the list values is not null nor empty
519             // 2. Explore action URL
520             return (!attributions.isNullOrEmpty() && attributions.any { !it.isNullOrEmpty() }) ||
521                 !commonWallpaperData.exploreActionUrl.isNullOrEmpty()
522         }
523 
524         private fun CreativeWallpaperData.getShareIntent(): Intent {
525             val shareIntent = Intent(Intent.ACTION_SEND)
526             shareIntent.putExtra(Intent.EXTRA_STREAM, shareUri)
527             shareIntent.setType("image/*")
528             shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
529             shareIntent.clipData = ClipData.newRawUri(null, shareUri)
530             return Intent.createChooser(shareIntent, null)
531         }
532 
533         private fun LiveWallpaperModel.canBeDeleted(): Boolean {
534             return if (creativeWallpaperData != null) {
535                 !liveWallpaperData.isApplied &&
536                     !creativeWallpaperData.isCurrent &&
537                     creativeWallpaperData.deleteUri.toString().isNotEmpty()
538             } else {
539                 !liveWallpaperData.isApplied
540             }
541         }
542 
543         /**
544          * @param isCreateNew: True means creating a new creative wallpaper. False means editing an
545          *   existing wallpaper.
546          */
547         fun LiveWallpaperData.getEditActivityIntent(isCreateNew: Boolean): Intent? {
548             val settingsActivity = systemWallpaperInfo.settingsActivity
549             if (settingsActivity.isNullOrEmpty()) {
550                 return null
551             }
552             val intent =
553                 Intent().apply {
554                     component = ComponentName(systemWallpaperInfo.packageName, settingsActivity)
555                     putExtra(WallpaperSettingsActivity.EXTRA_PREVIEW_MODE, true)
556                     putExtra(EXTRA_KEY_IS_CREATE_NEW, isCreateNew)
557                 }
558             return intent
559         }
560 
561         fun LiveWallpaperModel.isNewCreativeWallpaper(): Boolean {
562             return creativeWallpaperData?.deleteUri?.toString()?.isEmpty() == true
563         }
564 
565         /** The original combine function can only take up to 5 flows. */
566         inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine7(
567             flow: Flow<T1>,
568             flow2: Flow<T2>,
569             flow3: Flow<T3>,
570             flow4: Flow<T4>,
571             flow5: Flow<T5>,
572             flow6: Flow<T6>,
573             flow7: Flow<T7>,
574             crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R
575         ): Flow<R> {
576             return combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { args: Array<*> ->
577                 @Suppress("UNCHECKED_CAST")
578                 transform(
579                     args[0] as T1,
580                     args[1] as T2,
581                     args[2] as T3,
582                     args[3] as T4,
583                     args[4] as T5,
584                     args[5] as T6,
585                     args[6] as T7,
586                 )
587             }
588         }
589     }
590 }
591 
592 enum class Action {
593     INFORMATION,
594     DOWNLOAD,
595     DELETE,
596     EDIT,
597     CUSTOMIZE,
598     EFFECTS,
599     SHARE,
600 }
601