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 package com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel
17 
18 import com.android.intentresolver.contentpreview.CachingImagePreviewImageLoader
19 import com.android.intentresolver.contentpreview.HeadlineGenerator
20 import com.android.intentresolver.contentpreview.ImageLoader
21 import com.android.intentresolver.contentpreview.MimeTypeClassifier
22 import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.PayloadToggle
23 import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.ChooserRequestInteractor
24 import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.CustomActionsInteractor
25 import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.SelectablePreviewsInteractor
26 import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.SelectionInteractor
27 import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate
28 import com.android.intentresolver.contentpreview.payloadtoggle.shared.ContentType
29 import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
30 import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
31 import com.android.intentresolver.inject.ViewModelOwned
32 import dagger.Binds
33 import dagger.Module
34 import dagger.Provides
35 import dagger.hilt.InstallIn
36 import dagger.hilt.android.components.ViewModelComponent
37 import kotlinx.coroutines.CoroutineScope
38 import kotlinx.coroutines.flow.Flow
39 import kotlinx.coroutines.flow.SharingStarted
40 import kotlinx.coroutines.flow.flow
41 import kotlinx.coroutines.flow.map
42 import kotlinx.coroutines.flow.stateIn
43 import kotlinx.coroutines.flow.zip
44 
45 /** A dynamic carousel of selectable previews within share sheet. */
46 data class ShareouselViewModel(
47     /** Text displayed at the top of the share sheet when Shareousel is present. */
48     val headline: Flow<String>,
49     /** App-provided text shown beneath the headline. */
50     val metadataText: Flow<CharSequence?>,
51     /**
52      * Previews which are available for presentation within Shareousel. Use [preview] to create a
53      * [ShareouselPreviewViewModel] for a given [PreviewModel].
54      */
55     val previews: Flow<PreviewsModel?>,
56     /** List of action chips presented underneath Shareousel. */
57     val actions: Flow<List<ActionChipViewModel>>,
58     /** Creates a [ShareouselPreviewViewModel] for a [PreviewModel] present in [previews]. */
59     val preview:
60         (key: PreviewModel, index: Int?, scope: CoroutineScope) -> ShareouselPreviewViewModel,
61 )
62 
63 @Module
64 @InstallIn(ViewModelComponent::class)
65 interface ShareouselViewModelModule {
66 
67     @Binds @PayloadToggle fun imageLoader(imageLoader: CachingImagePreviewImageLoader): ImageLoader
68 
69     companion object {
70         @Provides
71         fun create(
72             interactor: SelectablePreviewsInteractor,
73             @PayloadToggle imageLoader: ImageLoader,
74             actionsInteractor: CustomActionsInteractor,
75             headlineGenerator: HeadlineGenerator,
76             selectionInteractor: SelectionInteractor,
77             chooserRequestInteractor: ChooserRequestInteractor,
78             mimeTypeClassifier: MimeTypeClassifier,
79             // TODO: remove if possible
80             @ViewModelOwned scope: CoroutineScope,
81         ): ShareouselViewModel {
82             val keySet =
83                 interactor.previews.stateIn(
84                     scope,
85                     SharingStarted.Eagerly,
86                     initialValue = null,
87                 )
88             return ShareouselViewModel(
89                 headline =
90                     selectionInteractor.aggregateContentType.zip(
91                         selectionInteractor.amountSelected
92                     ) { contentType, numItems ->
93                         when (contentType) {
94                             ContentType.Other -> headlineGenerator.getFilesHeadline(numItems)
95                             ContentType.Image -> headlineGenerator.getImagesHeadline(numItems)
96                             ContentType.Video -> headlineGenerator.getVideosHeadline(numItems)
97                         }
98                     },
99                 metadataText = chooserRequestInteractor.metadataText,
100                 previews = keySet,
101                 actions =
102                     actionsInteractor.customActions.map { actions ->
103                         actions.mapIndexedNotNull { i, model ->
104                             val icon = model.icon
105                             val label = model.label
106                             if (icon == null && label.isBlank()) {
107                                 null
108                             } else {
109                                 ActionChipViewModel(
110                                     label = label.toString(),
111                                     icon = model.icon,
112                                     onClicked = { model.performAction(i) },
113                                 )
114                             }
115                         }
116                     },
117                 preview = { key, index, previewScope ->
118                     keySet.value?.maybeLoad(index)
119                     val previewInteractor = interactor.preview(key)
120                     val contentType =
121                         when {
122                             mimeTypeClassifier.isImageType(key.mimeType) -> ContentType.Image
123                             mimeTypeClassifier.isVideoType(key.mimeType) -> ContentType.Video
124                             else -> ContentType.Other
125                         }
126                     val initialBitmapValue =
127                         key.previewUri?.let {
128                             imageLoader.getCachedBitmap(it)?.let { ValueUpdate.Value(it) }
129                         } ?: ValueUpdate.Absent
130                     ShareouselPreviewViewModel(
131                         bitmapLoadState =
132                             flow {
133                                     emit(
134                                         key.previewUri?.let { ValueUpdate.Value(imageLoader(it)) }
135                                             ?: ValueUpdate.Absent
136                                     )
137                                 }
138                                 .stateIn(previewScope, SharingStarted.Eagerly, initialBitmapValue),
139                         contentType = contentType,
140                         isSelected = previewInteractor.isSelected,
141                         setSelected = previewInteractor::setSelected,
142                         aspectRatio = key.aspectRatio,
143                     )
144                 },
145             )
146         }
147     }
148 }
149 
PreviewsModelnull150 private fun PreviewsModel.maybeLoad(index: Int?) {
151     when {
152         index == null -> {}
153         index <= leftTriggerIndex -> loadMoreLeft?.invoke()
154         index >= rightTriggerIndex -> loadMoreRight?.invoke()
155     }
156 }
157