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