1 /* <lambda>null2 * Copyright (C) 2023 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 package com.android.intentresolver.contentpreview 18 19 import android.net.Uri 20 import android.view.LayoutInflater 21 import android.view.View 22 import android.view.ViewGroup 23 import android.widget.CheckBox 24 import android.widget.TextView 25 import androidx.annotation.IdRes 26 import androidx.test.ext.junit.runners.AndroidJUnit4 27 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation 28 import com.android.intentresolver.R 29 import com.android.intentresolver.widget.ActionRow 30 import com.google.common.truth.Truth.assertThat 31 import com.google.common.truth.Truth.assertWithMessage 32 import java.util.function.Consumer 33 import kotlin.coroutines.EmptyCoroutineContext 34 import kotlinx.coroutines.test.TestScope 35 import kotlinx.coroutines.test.UnconfinedTestDispatcher 36 import org.junit.Test 37 import org.junit.runner.RunWith 38 import org.mockito.kotlin.any 39 import org.mockito.kotlin.doReturn 40 import org.mockito.kotlin.mock 41 import org.mockito.kotlin.never 42 import org.mockito.kotlin.times 43 import org.mockito.kotlin.verify 44 45 private const val HEADLINE_IMAGES = "Image Headline" 46 private const val HEADLINE_VIDEOS = "Video Headline" 47 private const val HEADLINE_FILES = "Files Headline" 48 private const val SHARED_TEXT = "Some text to share" 49 50 @RunWith(AndroidJUnit4::class) 51 class FilesPlusTextContentPreviewUiTest { 52 private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher()) 53 private val actionFactory = 54 object : ChooserContentPreviewUi.ActionFactory { 55 override fun getEditButtonRunnable(): Runnable? = null 56 57 override fun getCopyButtonRunnable(): Runnable? = null 58 59 override fun createCustomActions(): List<ActionRow.Action> = emptyList() 60 61 override fun getModifyShareAction(): ActionRow.Action? = null 62 63 override fun getExcludeSharedTextAction(): Consumer<Boolean> = Consumer<Boolean> {} 64 } 65 private val imageLoader = mock<ImageLoader>() 66 private val headlineGenerator = 67 mock<HeadlineGenerator> { 68 on { getImagesHeadline(any()) } doReturn HEADLINE_IMAGES 69 on { getVideosHeadline(any()) } doReturn HEADLINE_VIDEOS 70 on { getFilesHeadline(any()) } doReturn HEADLINE_FILES 71 } 72 private val testMetadataText: CharSequence = "Test metadata text" 73 74 private val context 75 get() = getInstrumentation().context 76 77 @Test 78 fun test_displayImagesPlusTextWithoutUriMetadataHeader_showImagesHeadline() { 79 val sharedFileCount = 2 80 val (previewView, headlineRow) = testLoadingHeadline("image/*", sharedFileCount) 81 82 assertWithMessage("Preview parent should not be null").that(previewView).isNotNull() 83 verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount) 84 verifyPreviewHeadline(headlineRow, HEADLINE_IMAGES) 85 verifyPreviewMetadata(headlineRow, testMetadataText) 86 verifySharedText(previewView) 87 } 88 89 @Test 90 fun test_displayVideosPlusTextWithoutUriMetadataHeader_showVideosHeadline() { 91 val sharedFileCount = 2 92 val (previewView, headlineRow) = testLoadingHeadline("video/*", sharedFileCount) 93 94 verify(headlineGenerator, times(1)).getVideosHeadline(sharedFileCount) 95 assertWithMessage("Preview parent should not be null").that(previewView).isNotNull() 96 verifyPreviewHeadline(headlineRow, HEADLINE_VIDEOS) 97 verifyPreviewMetadata(headlineRow, testMetadataText) 98 verifySharedText(previewView) 99 } 100 101 @Test 102 fun test_displayDocsPlusTextWithoutUriMetadataHeader_showFilesHeadline() { 103 val sharedFileCount = 2 104 val (previewView, headlineRow) = testLoadingHeadline("application/pdf", sharedFileCount) 105 106 verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount) 107 assertWithMessage("Preview parent should not be null").that(previewView).isNotNull() 108 verifyPreviewHeadline(headlineRow, HEADLINE_FILES) 109 verifyPreviewMetadata(headlineRow, testMetadataText) 110 verifySharedText(previewView) 111 } 112 113 @Test 114 fun test_displayMixedContentPlusTextWithoutUriMetadataHeader_showFilesHeadline() { 115 val sharedFileCount = 2 116 val (previewView, headlineRow) = testLoadingHeadline("*/*", sharedFileCount) 117 118 verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount) 119 assertWithMessage("Preview parent should not be null").that(previewView).isNotNull() 120 verifyPreviewHeadline(headlineRow, HEADLINE_FILES) 121 verifyPreviewMetadata(headlineRow, testMetadataText) 122 verifySharedText(previewView) 123 } 124 125 @Test 126 fun test_displayImagesPlusTextWithUriMetadataSetHeader_showImagesHeadline() { 127 val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "image/jpeg") 128 val sharedFileCount = loadedFileMetadata.size 129 val (previewView, headlineRow) = 130 testLoadingHeadline("image/*", sharedFileCount, loadedFileMetadata) 131 132 verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount) 133 assertWithMessage("Preview parent should not be null").that(previewView).isNotNull() 134 verifyPreviewHeadline(headlineRow, HEADLINE_IMAGES) 135 verifyPreviewMetadata(headlineRow, testMetadataText) 136 verifySharedText(previewView) 137 } 138 139 @Test 140 fun test_displayVideosPlusTextWithUriMetadataSetHeader_showVideosHeadline() { 141 val loadedFileMetadata = createFileInfosWithMimeTypes("video/mp4", "video/mp4") 142 val sharedFileCount = loadedFileMetadata.size 143 val (previewView, headlineRow) = 144 testLoadingHeadline("video/*", sharedFileCount, loadedFileMetadata) 145 146 verify(headlineGenerator, times(1)).getVideosHeadline(sharedFileCount) 147 assertWithMessage("Preview parent should not be null").that(previewView).isNotNull() 148 verifyPreviewHeadline(headlineRow, HEADLINE_VIDEOS) 149 verifyPreviewMetadata(headlineRow, testMetadataText) 150 verifySharedText(previewView) 151 } 152 153 @Test 154 fun test_displayImagesAndVideosPlusTextWithUriMetadataSetHeader_showFilesHeadline() { 155 val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "video/mp4") 156 val sharedFileCount = loadedFileMetadata.size 157 val (previewView, headlineRow) = 158 testLoadingHeadline("*/*", sharedFileCount, loadedFileMetadata) 159 160 verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount) 161 assertWithMessage("Preview parent should not be null").that(previewView).isNotNull() 162 verifyPreviewHeadline(headlineRow, HEADLINE_FILES) 163 verifyPreviewMetadata(headlineRow, testMetadataText) 164 verifySharedText(previewView) 165 } 166 167 @Test 168 fun test_displayDocsPlusTextWithUriMetadataSetHeader_showFilesHeadline() { 169 val loadedFileMetadata = createFileInfosWithMimeTypes("application/pdf", "application/pdf") 170 val sharedFileCount = loadedFileMetadata.size 171 val (previewView, headlineRow) = 172 testLoadingHeadline("application/pdf", sharedFileCount, loadedFileMetadata) 173 174 verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount) 175 assertWithMessage("Preview parent should not be null").that(previewView).isNotNull() 176 verifyPreviewHeadline(headlineRow, HEADLINE_FILES) 177 verifyPreviewMetadata(headlineRow, testMetadataText) 178 verifySharedText(previewView) 179 } 180 181 @Test 182 fun test_uriMetadataIsMoreSpecificThanIntentMimeType_headlineGetsUpdated() { 183 val sharedFileCount = 2 184 val testSubject = 185 FilesPlusTextContentPreviewUi( 186 testScope, 187 /*isSingleImage=*/ false, 188 sharedFileCount, 189 SHARED_TEXT, 190 /*intentMimeType=*/ "*/*", 191 actionFactory, 192 imageLoader, 193 DefaultMimeTypeClassifier, 194 headlineGenerator, 195 testMetadataText, 196 /* allowTextToggle=*/ false, 197 ) 198 val layoutInflater = LayoutInflater.from(context) 199 val gridLayout = 200 layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false) 201 as ViewGroup 202 val headlineRow = gridLayout.requireViewById<View>(R.id.chooser_headline_row_container) 203 204 testSubject.display( 205 context.resources, 206 LayoutInflater.from(context), 207 gridLayout, 208 headlineRow, 209 ) 210 211 verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount) 212 verify(headlineGenerator, never()).getImagesHeadline(sharedFileCount) 213 verifyPreviewHeadline(headlineRow, HEADLINE_FILES) 214 verifyPreviewMetadata(headlineRow, testMetadataText) 215 216 testSubject.updatePreviewMetadata(createFileInfosWithMimeTypes("image/png", "image/jpg")) 217 218 verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount) 219 verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount) 220 verifyPreviewHeadline(headlineRow, HEADLINE_IMAGES) 221 verifyPreviewMetadata(headlineRow, testMetadataText) 222 } 223 224 @Test 225 fun test_uriMetadataIsMoreSpecificThanIntentMimeTypeHeader_headlineGetsUpdated() { 226 val sharedFileCount = 2 227 val testSubject = 228 FilesPlusTextContentPreviewUi( 229 testScope, 230 /*isSingleImage=*/ false, 231 sharedFileCount, 232 SHARED_TEXT, 233 /*intentMimeType=*/ "*/*", 234 actionFactory, 235 imageLoader, 236 DefaultMimeTypeClassifier, 237 headlineGenerator, 238 testMetadataText, 239 /* allowTextToggle=*/ false, 240 ) 241 val layoutInflater = LayoutInflater.from(context) 242 val gridLayout = 243 layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false) 244 as ViewGroup 245 val headlineRow = gridLayout.requireViewById<View>(R.id.chooser_headline_row_container) 246 247 assertWithMessage("Headline should not be inflated by default") 248 .that(headlineRow.findViewById<View>(R.id.headline)) 249 .isNull() 250 assertWithMessage("Metadata should not be inflated by default") 251 .that(headlineRow.findViewById<View>(R.id.metadata)) 252 .isNull() 253 254 val previewView = 255 testSubject.display( 256 context.resources, 257 LayoutInflater.from(context), 258 gridLayout, 259 headlineRow, 260 ) 261 262 verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount) 263 verify(headlineGenerator, never()).getImagesHeadline(sharedFileCount) 264 assertWithMessage("Preview parent should not be null").that(previewView).isNotNull() 265 verifyPreviewHeadline(headlineRow, HEADLINE_FILES) 266 verifyPreviewMetadata(headlineRow, testMetadataText) 267 268 testSubject.updatePreviewMetadata(createFileInfosWithMimeTypes("image/png", "image/jpg")) 269 270 verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount) 271 verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount) 272 verifyPreviewHeadline(headlineRow, HEADLINE_IMAGES) 273 verifyPreviewMetadata(headlineRow, testMetadataText) 274 } 275 276 @Test 277 fun test_allowToggle() { 278 val testSubject = 279 FilesPlusTextContentPreviewUi( 280 testScope, 281 /*isSingleImage=*/ false, 282 /* fileCount=*/ 1, 283 SHARED_TEXT, 284 /*intentMimeType=*/ "*/*", 285 actionFactory, 286 imageLoader, 287 DefaultMimeTypeClassifier, 288 headlineGenerator, 289 testMetadataText, 290 /* allowTextToggle=*/ true, 291 ) 292 val layoutInflater = LayoutInflater.from(context) 293 val gridLayout = 294 layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false) 295 as ViewGroup 296 val headlineRow = gridLayout.requireViewById<View>(R.id.chooser_headline_row_container) 297 298 testSubject.display( 299 context.resources, 300 LayoutInflater.from(context), 301 gridLayout, 302 headlineRow, 303 ) 304 305 val checkbox = headlineRow.requireViewById<CheckBox>(R.id.include_text_action) 306 assertThat(checkbox.visibility).isEqualTo(View.VISIBLE) 307 assertThat(checkbox.isChecked).isTrue() 308 } 309 310 @Test 311 fun test_hideTextToggle() { 312 val testSubject = 313 FilesPlusTextContentPreviewUi( 314 testScope, 315 /*isSingleImage=*/ false, 316 /* fileCount=*/ 1, 317 SHARED_TEXT, 318 /*intentMimeType=*/ "*/*", 319 actionFactory, 320 imageLoader, 321 DefaultMimeTypeClassifier, 322 headlineGenerator, 323 testMetadataText, 324 /* allowTextToggle=*/ false, 325 ) 326 val layoutInflater = LayoutInflater.from(context) 327 val gridLayout = 328 layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false) 329 as ViewGroup 330 val headlineRow = gridLayout.requireViewById<View>(R.id.chooser_headline_row_container) 331 332 testSubject.display( 333 context.resources, 334 LayoutInflater.from(context), 335 gridLayout, 336 headlineRow, 337 ) 338 339 val checkbox = headlineRow.requireViewById<CheckBox>(R.id.include_text_action) 340 assertThat(checkbox.visibility).isNotEqualTo(View.VISIBLE) 341 } 342 343 private fun testLoadingHeadline( 344 intentMimeType: String, 345 sharedFileCount: Int, 346 loadedFileMetadata: List<FileInfo>? = null, 347 ): Pair<ViewGroup?, View> { 348 val testSubject = 349 FilesPlusTextContentPreviewUi( 350 testScope, 351 /*isSingleImage=*/ false, 352 sharedFileCount, 353 SHARED_TEXT, 354 intentMimeType, 355 actionFactory, 356 imageLoader, 357 DefaultMimeTypeClassifier, 358 headlineGenerator, 359 testMetadataText, 360 /* allowTextToggle=*/ false, 361 ) 362 val layoutInflater = LayoutInflater.from(context) 363 val gridLayout = 364 layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false) 365 as ViewGroup 366 val headlineRow = gridLayout.requireViewById<View>(R.id.chooser_headline_row_container) 367 368 assertWithMessage("Headline should not be inflated by default") 369 .that(headlineRow.findViewById<View>(R.id.headline)) 370 .isNull() 371 372 assertWithMessage("Metadata should not be inflated by default") 373 .that(headlineRow.findViewById<View>(R.id.metadata)) 374 .isNull() 375 376 loadedFileMetadata?.let(testSubject::updatePreviewMetadata) 377 return testSubject.display( 378 context.resources, 379 LayoutInflater.from(context), 380 gridLayout, 381 headlineRow, 382 ) to headlineRow 383 } 384 385 private fun createFileInfosWithMimeTypes(vararg mimeTypes: String): List<FileInfo> { 386 val uri = Uri.parse("content://pkg.app/file") 387 return mimeTypes.map { mimeType -> FileInfo.Builder(uri).withMimeType(mimeType).build() } 388 } 389 390 private fun verifyTextViewText( 391 parentView: View?, 392 @IdRes textViewResId: Int, 393 expectedText: CharSequence, 394 ) { 395 assertThat(parentView).isNotNull() 396 val textView = parentView?.findViewById<TextView>(textViewResId) 397 assertThat(textView).isNotNull() 398 assertThat(textView?.text).isEqualTo(expectedText) 399 } 400 401 private fun verifyPreviewHeadline(headerViewParent: View?, expectedText: String) { 402 verifyTextViewText(headerViewParent, R.id.headline, expectedText) 403 } 404 405 private fun verifyPreviewMetadata(headerViewParent: View?, expectedText: CharSequence) { 406 verifyTextViewText(headerViewParent, R.id.metadata, expectedText) 407 } 408 409 private fun verifySharedText(previewView: ViewGroup?) { 410 verifyTextViewText(previewView, R.id.content_preview_text, SHARED_TEXT) 411 } 412 } 413