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