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