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 
17 @file:OptIn(ExperimentalCoroutinesApi::class)
18 
19 package com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel
20 
21 import android.app.Activity
22 import android.content.Intent
23 import android.graphics.Bitmap
24 import android.graphics.drawable.Icon
25 import android.net.Uri
26 import com.android.intentresolver.FakeImageLoader
27 import com.android.intentresolver.contentpreview.HeadlineGenerator
28 import com.android.intentresolver.contentpreview.mimetypeClassifier
29 import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel
30 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.activityResultRepository
31 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.cursorPreviewsRepository
32 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.previewSelectionsRepository
33 import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.PendingIntentSender
34 import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.TargetIntentModifier
35 import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.pendingIntentSender
36 import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.targetIntentModifier
37 import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.chooserRequestInteractor
38 import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.customActionsInteractor
39 import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.headlineGenerator
40 import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.payloadToggleImageLoader
41 import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.selectablePreviewsInteractor
42 import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.selectionInteractor
43 import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate
44 import com.android.intentresolver.contentpreview.payloadtoggle.shared.ContentType
45 import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewKey
46 import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
47 import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
48 import com.android.intentresolver.data.model.ChooserRequest
49 import com.android.intentresolver.data.repository.chooserRequestRepository
50 import com.android.intentresolver.icon.BitmapIcon
51 import com.android.intentresolver.logging.FakeEventLog
52 import com.android.intentresolver.logging.eventLog
53 import com.android.intentresolver.util.KosmosTestScope
54 import com.android.intentresolver.util.comparingElementsUsingTransform
55 import com.android.intentresolver.util.runKosmosTest
56 import com.android.systemui.kosmos.Kosmos
57 import com.android.systemui.kosmos.Kosmos.Fixture
58 import com.google.common.truth.Truth.assertThat
59 import com.google.common.truth.Truth.assertWithMessage
60 import kotlinx.coroutines.CoroutineScope
61 import kotlinx.coroutines.ExperimentalCoroutinesApi
62 import kotlinx.coroutines.flow.first
63 import org.junit.Test
64 
65 class ShareouselViewModelTest {
66 
67     private var Kosmos.viewModelScope: CoroutineScope by Fixture()
68     private val Kosmos.shareouselViewModel: ShareouselViewModel by Fixture {
69         ShareouselViewModelModule.create(
70             interactor = selectablePreviewsInteractor,
71             imageLoader = payloadToggleImageLoader,
72             actionsInteractor = customActionsInteractor,
73             headlineGenerator = headlineGenerator,
74             chooserRequestInteractor = chooserRequestInteractor,
75             mimeTypeClassifier = mimetypeClassifier,
76             selectionInteractor = selectionInteractor,
77             scope = viewModelScope,
78         )
79     }
80     private val previewHeight = 500
81 
82     @Test
83     fun headline_images() = runTest {
84         assertThat(shareouselViewModel.headline.first()).isEqualTo("FILES: 1")
85         previewSelectionsRepository.selections.value =
86             listOf(
87                     PreviewModel(
88                         key = PreviewKey.final(0),
89                         uri = Uri.fromParts("scheme", "ssp", "fragment"),
90                         mimeType = "image/png",
91                         order = 0,
92                     ),
93                     PreviewModel(
94                         key = PreviewKey.final(1),
95                         uri = Uri.fromParts("scheme1", "ssp1", "fragment1"),
96                         mimeType = "image/jpeg",
97                         order = 1,
98                     ),
99                 )
100                 .associateBy { it.uri }
101         runCurrent()
102         assertThat(shareouselViewModel.headline.first()).isEqualTo("IMAGES: 2")
103     }
104 
105     @Test
106     fun headline_videos() = runTest {
107         previewSelectionsRepository.selections.value =
108             listOf(
109                     PreviewModel(
110                         key = PreviewKey.final(0),
111                         uri = Uri.fromParts("scheme", "ssp", "fragment"),
112                         mimeType = "video/mpeg",
113                         order = 0,
114                     ),
115                     PreviewModel(
116                         key = PreviewKey.final(1),
117                         uri = Uri.fromParts("scheme1", "ssp1", "fragment1"),
118                         mimeType = "video/mpeg",
119                         order = 1,
120                     ),
121                 )
122                 .associateBy { it.uri }
123         runCurrent()
124         assertThat(shareouselViewModel.headline.first()).isEqualTo("VIDEOS: 2")
125     }
126 
127     @Test
128     fun headline_mixed() = runTest {
129         previewSelectionsRepository.selections.value =
130             listOf(
131                     PreviewModel(
132                         key = PreviewKey.final(0),
133                         uri = Uri.fromParts("scheme", "ssp", "fragment"),
134                         mimeType = "image/jpeg",
135                         order = 0,
136                     ),
137                     PreviewModel(
138                         key = PreviewKey.final(1),
139                         uri = Uri.fromParts("scheme1", "ssp1", "fragment1"),
140                         mimeType = "video/mpeg",
141                         order = 1,
142                     ),
143                 )
144                 .associateBy { it.uri }
145         runCurrent()
146         assertThat(shareouselViewModel.headline.first()).isEqualTo("FILES: 2")
147     }
148 
149     @Test
150     fun metadataText() = runTest {
151         val request =
152             ChooserRequest(
153                 targetIntent = Intent(),
154                 launchedFromPackage = "",
155                 metadataText = "Hello",
156             )
157         chooserRequestRepository.chooserRequest.value = request
158 
159         runCurrent()
160 
161         assertThat(shareouselViewModel.metadataText.first()).isEqualTo("Hello")
162     }
163 
164     @Test
165     fun previews() =
166         runTest(targetIntentModifier = { Intent() }) {
167             cursorPreviewsRepository.previewsModel.value =
168                 PreviewsModel(
169                     previewModels =
170                         listOf(
171                             PreviewModel(
172                                 key = PreviewKey.final(0),
173                                 uri = Uri.fromParts("scheme", "ssp", "fragment"),
174                                 mimeType = "image/png",
175                                 order = 0,
176                             ),
177                             PreviewModel(
178                                 key = PreviewKey.final(1),
179                                 uri = Uri.fromParts("scheme1", "ssp1", "fragment1"),
180                                 mimeType = "video/mpeg",
181                                 order = 1,
182                             ),
183                         ),
184                     startIdx = 1,
185                     loadMoreLeft = null,
186                     loadMoreRight = null,
187                     leftTriggerIndex = 0,
188                     rightTriggerIndex = 1,
189                 )
190             runCurrent()
191 
192             assertWithMessage("previewsKeys is null")
193                 .that(shareouselViewModel.previews.first())
194                 .isNotNull()
195             assertThat(shareouselViewModel.previews.first()!!.previewModels)
196                 .comparingElementsUsingTransform("has uri of") { it: PreviewModel -> it.uri }
197                 .containsExactly(
198                     Uri.fromParts("scheme", "ssp", "fragment"),
199                     Uri.fromParts("scheme1", "ssp1", "fragment1"),
200                 )
201                 .inOrder()
202 
203             val previewVm =
204                 shareouselViewModel.preview.invoke(
205                     PreviewModel(
206                         key = PreviewKey.final(1),
207                         uri = Uri.fromParts("scheme1", "ssp1", "fragment1"),
208                         mimeType = "video/mpeg",
209                         order = 0,
210                     ),
211                     previewHeight,
212                     /* index = */ 1,
213                     viewModelScope,
214                 )
215 
216             runCurrent()
217 
218             assertWithMessage("preview bitmap is null")
219                 .that((previewVm.bitmapLoadState.first() as ValueUpdate.Value).value)
220                 .isNotNull()
221             assertThat(previewVm.isSelected.first()).isFalse()
222             assertThat(previewVm.contentType).isEqualTo(ContentType.Video)
223 
224             previewVm.setSelected(true)
225 
226             assertThat(previewSelectionsRepository.selections.value.keys)
227                 .contains(Uri.fromParts("scheme1", "ssp1", "fragment1"))
228         }
229 
230     @Test
231     fun previews_wontLoad() =
232         runTest(targetIntentModifier = { Intent() }) {
233             cursorPreviewsRepository.previewsModel.value =
234                 PreviewsModel(
235                     previewModels =
236                         listOf(
237                             PreviewModel(
238                                 key = PreviewKey.final(0),
239                                 uri = Uri.fromParts("scheme", "ssp", "fragment"),
240                                 mimeType = "image/png",
241                                 order = 0,
242                             ),
243                             PreviewModel(
244                                 key = PreviewKey.final(1),
245                                 uri = Uri.fromParts("scheme1", "ssp1", "fragment1"),
246                                 mimeType = "video/mpeg",
247                                 order = 1,
248                             ),
249                         ),
250                     startIdx = 1,
251                     loadMoreLeft = null,
252                     loadMoreRight = null,
253                     leftTriggerIndex = 0,
254                     rightTriggerIndex = 1,
255                 )
256             runCurrent()
257 
258             val previewVm =
259                 shareouselViewModel.preview.invoke(
260                     PreviewModel(
261                         key = PreviewKey.final(0),
262                         uri = Uri.fromParts("scheme", "ssp", "fragment"),
263                         mimeType = "video/mpeg",
264                         order = 1,
265                     ),
266                     previewHeight,
267                     /* index = */ 1,
268                     viewModelScope,
269                 )
270 
271             runCurrent()
272 
273             assertWithMessage("preview bitmap is not null")
274                 .that((previewVm.bitmapLoadState.first() as ValueUpdate.Value).value)
275                 .isNull()
276         }
277 
278     @Test
279     fun actions() {
280         runTest {
281             assertThat(shareouselViewModel.actions.first()).isEmpty()
282 
283             val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8)
284             val icon = Icon.createWithBitmap(bitmap)
285             var actionSent = false
286             chooserRequestRepository.customActions.value =
287                 listOf(
288                     CustomActionModel(
289                         label = "label1",
290                         icon = icon,
291                         performAction = { actionSent = true },
292                     )
293                 )
294             runCurrent()
295 
296             assertThat(shareouselViewModel.actions.first())
297                 .comparingElementsUsingTransform("has a label of") { vm: ActionChipViewModel ->
298                     vm.label
299                 }
300                 .containsExactly("label1")
301                 .inOrder()
302             assertThat(shareouselViewModel.actions.first())
303                 .comparingElementsUsingTransform("has an icon of") { vm: ActionChipViewModel ->
304                     vm.icon
305                 }
306                 .containsExactly(BitmapIcon(icon.bitmap))
307                 .inOrder()
308 
309             shareouselViewModel.actions.first()[0].onClicked()
310 
311             assertThat(actionSent).isTrue()
312             assertThat(eventLog.customActionSelected)
313                 .isEqualTo(FakeEventLog.CustomActionSelected(0))
314             assertThat(activityResultRepository.activityResult.value).isEqualTo(Activity.RESULT_OK)
315         }
316     }
317 
318     private fun runTest(
319         pendingIntentSender: PendingIntentSender = PendingIntentSender {},
320         targetIntentModifier: TargetIntentModifier<PreviewModel> = TargetIntentModifier {
321             error("unexpected invocation")
322         },
323         block: suspend KosmosTestScope.() -> Unit,
324     ): Unit = runKosmosTest {
325         viewModelScope = backgroundScope
326         this.pendingIntentSender = pendingIntentSender
327         this.targetIntentModifier = targetIntentModifier
328         previewSelectionsRepository.selections.value =
329             PreviewModel(
330                     key = PreviewKey.final(1),
331                     uri = Uri.fromParts("scheme", "ssp", "fragment"),
332                     mimeType = null,
333                     order = 0,
334                 )
335                 .let { mapOf(it.uri to it) }
336         payloadToggleImageLoader =
337             FakeImageLoader(
338                 initialBitmaps =
339                     mapOf(
340                         Uri.fromParts("scheme1", "ssp1", "fragment1") to
341                             Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8)
342                     )
343             )
344         headlineGenerator =
345             object : HeadlineGenerator {
346                 override fun getImagesHeadline(count: Int): String = "IMAGES: $count"
347 
348                 override fun getTextHeadline(text: CharSequence): String = error("not supported")
349 
350                 override fun getAlbumHeadline(): String = error("not supported")
351 
352                 override fun getImagesWithTextHeadline(text: CharSequence, count: Int): String =
353                     error("not supported")
354 
355                 override fun getVideosWithTextHeadline(text: CharSequence, count: Int): String =
356                     error("not supported")
357 
358                 override fun getFilesWithTextHeadline(text: CharSequence, count: Int): String =
359                     error("not supported")
360 
361                 override fun getVideosHeadline(count: Int): String = "VIDEOS: $count"
362 
363                 override fun getFilesHeadline(count: Int): String = "FILES: $count"
364 
365                 override fun getNotItemsSelectedHeadline() = "Select items to share"
366             }
367         // instantiate the view model, and then runCurrent() so that it is fully hydrated before
368         // starting the test
369         shareouselViewModel
370         runCurrent()
371         block()
372     }
373 }
374