1 /* 2 * Copyright (C) 2021 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.providers.media.photopicker.viewmodel; 18 19 import static android.provider.CloudMediaProviderContract.AlbumColumns; 20 import static android.provider.CloudMediaProviderContract.MediaColumns; 21 22 import static com.google.common.truth.Truth.assertThat; 23 24 import static org.mockito.Mockito.mock; 25 import static org.mockito.Mockito.when; 26 27 import android.app.Application; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.database.Cursor; 31 import android.database.MatrixCursor; 32 import android.net.Uri; 33 import android.provider.CloudMediaProviderContract; 34 import android.text.format.DateUtils; 35 36 import androidx.annotation.NonNull; 37 import androidx.annotation.Nullable; 38 import androidx.lifecycle.LiveData; 39 import androidx.test.InstrumentationRegistry; 40 import androidx.test.runner.AndroidJUnit4; 41 42 import com.android.providers.media.photopicker.PickerSyncController; 43 import com.android.providers.media.photopicker.data.ItemsProvider; 44 import com.android.providers.media.photopicker.data.UserIdManager; 45 import com.android.providers.media.photopicker.data.model.Category; 46 import com.android.providers.media.photopicker.data.model.Item; 47 import com.android.providers.media.photopicker.data.model.ItemTest; 48 import com.android.providers.media.photopicker.data.model.UserId; 49 import com.android.providers.media.util.ForegroundThread; 50 51 import org.junit.Before; 52 import org.junit.Rule; 53 import org.junit.Test; 54 import org.junit.runner.RunWith; 55 import org.mockito.Mock; 56 import org.mockito.MockitoAnnotations; 57 58 import java.util.ArrayList; 59 import java.util.List; 60 61 @RunWith(AndroidJUnit4.class) 62 public class PickerViewModelTest { 63 64 private static final String FAKE_IMAGE_MIME_TYPE = "image/jpg"; 65 private static final String FAKE_CATEGORY_NAME = "testCategoryName"; 66 private static final String FAKE_ID = "5"; 67 68 private static final Category FAKE_CATEGORY = 69 new Category(FAKE_ID, PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, 70 FAKE_CATEGORY_NAME, Uri.parse("content://media/foo"), 0, true); 71 72 @Rule 73 public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule(); 74 75 @Mock 76 private Application mApplication; 77 78 private PickerViewModel mPickerViewModel; 79 private TestItemsProvider mItemsProvider; 80 81 @Before setUp()82 public void setUp() { 83 MockitoAnnotations.initMocks(this); 84 85 final Context context = InstrumentationRegistry.getTargetContext(); 86 when(mApplication.getApplicationContext()).thenReturn(context); 87 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 88 mPickerViewModel = new PickerViewModel(mApplication); 89 }); 90 mItemsProvider = new TestItemsProvider(context); 91 mPickerViewModel.setItemsProvider(mItemsProvider); 92 UserIdManager userIdManager = mock(UserIdManager.class); 93 when(userIdManager.getCurrentUserProfileId()).thenReturn(UserId.CURRENT_USER); 94 mPickerViewModel.setUserIdManager(userIdManager); 95 } 96 97 @Test testGetItems_noItems()98 public void testGetItems_noItems() throws Exception { 99 final int itemCount = 0; 100 mItemsProvider.setItems(generateFakeImageItemList(itemCount)); 101 mPickerViewModel.updateItems(); 102 // We use ForegroundThread to execute the loadItems in updateItems(), wait for the thread 103 // idle 104 ForegroundThread.waitForIdle(); 105 106 final List<Item> itemList = mPickerViewModel.getItems().getValue(); 107 108 // No date headers, the size should be 0 109 assertThat(itemList.size()).isEqualTo(itemCount); 110 } 111 112 @Test testGetItems_hasRecentItem()113 public void testGetItems_hasRecentItem() throws Exception { 114 final int itemCount = 1; 115 final List<Item> fakeItemList = generateFakeImageItemList(itemCount); 116 final Item fakeItem = fakeItemList.get(0); 117 mItemsProvider.setItems(generateFakeImageItemList(itemCount)); 118 mPickerViewModel.updateItems(); 119 // We use ForegroundThread to execute the loadItems in updateItems(), wait for the thread 120 // idle 121 ForegroundThread.waitForIdle(); 122 123 final List<Item> itemList = mPickerViewModel.getItems().getValue(); 124 125 // Original one item + 1 Recent item 126 assertThat(itemList.size()).isEqualTo(itemCount + 1); 127 // Check the first item is recent item 128 final Item recentItem = itemList.get(0); 129 assertThat(recentItem.isDate()).isTrue(); 130 assertThat(recentItem.getDateTaken()).isEqualTo(0); 131 // Check the second item is fakeItem 132 final Item firstPhotoItem = itemList.get(1); 133 assertThat(firstPhotoItem.getId()).isEqualTo(fakeItem.getId()); 134 } 135 136 @Test testGetItems_exceedMinCount_notSameDay_hasRecentItemAndOneDateItem()137 public void testGetItems_exceedMinCount_notSameDay_hasRecentItemAndOneDateItem() 138 throws Exception { 139 final int itemCount = 13; 140 mItemsProvider.setItems(generateFakeImageItemList(itemCount)); 141 mPickerViewModel.updateItems(); 142 // We use ForegroundThread to execute the loadItems in updateItems(), wait for the thread 143 // idle 144 ForegroundThread.waitForIdle(); 145 146 final List<Item> itemList = mPickerViewModel.getItems().getValue(); 147 148 // Original item count + 1 Recent item + 1 date item 149 assertThat(itemList.size()).isEqualTo(itemCount + 2); 150 assertThat(itemList.get(0).isDate()).isTrue(); 151 assertThat(itemList.get(0).getDateTaken()).isEqualTo(0); 152 // The index 13 is the next date header because the minimum item count in recent section is 153 // 12 154 assertThat(itemList.get(13).isDate()).isTrue(); 155 assertThat(itemList.get(13).getDateTaken()).isNotEqualTo(0); 156 } 157 158 /** 159 * Test that The total number in `Recent` may exceed the minimum count. If the photo items are 160 * taken on same day, they should not be split apart. 161 */ 162 @Test testGetItems_exceedMinCount_sameDay_hasRecentItemNoDateItem()163 public void testGetItems_exceedMinCount_sameDay_hasRecentItemNoDateItem() throws Exception { 164 final int originalItemCount = 12; 165 final String lastItemId = "13"; 166 final List<Item> fakeItemList = generateFakeImageItemList(originalItemCount); 167 final long dateTakenMs = fakeItemList.get(originalItemCount - 1).getDateTaken(); 168 final long generationModified = 1L; 169 final Item lastItem = ItemTest.generateItem(lastItemId, FAKE_IMAGE_MIME_TYPE, 170 dateTakenMs, generationModified, /* duration= */ 1000L); 171 fakeItemList.add(lastItem); 172 final int itemCount = fakeItemList.size(); 173 mItemsProvider.setItems(fakeItemList); 174 mPickerViewModel.updateItems(); 175 // We use ForegroundThread to execute the loadItems in updateItems(), wait for the thread 176 // idle 177 ForegroundThread.waitForIdle(); 178 179 final List<Item> itemList = mPickerViewModel.getItems().getValue(); 180 181 // Original item count + 1 new Recent item 182 assertThat(itemList.size()).isEqualTo(itemCount + 1); 183 assertThat(itemList.get(0).isDate()).isTrue(); 184 assertThat(itemList.get(0).getDateTaken()).isEqualTo(0); 185 } 186 187 @Test testGetCategoryItems()188 public void testGetCategoryItems() throws Exception { 189 final int itemCount = 3; 190 final LiveData<List<Item>> categoryItems = mPickerViewModel.getCategoryItems(FAKE_CATEGORY); 191 mItemsProvider.setItems(generateFakeImageItemList(itemCount)); 192 mPickerViewModel.updateCategoryItems(); 193 // We use ForegroundThread to execute the loadItems in updateCategoryItems(), wait for the 194 // thread idle 195 ForegroundThread.waitForIdle(); 196 197 final List<Item> itemList = categoryItems.getValue(); 198 199 // Original item count + 3 date items 200 assertThat(itemList.size()).isEqualTo(itemCount + 3); 201 // Test the first item is date item 202 final Item firstDateItem = itemList.get(0); 203 assertThat(firstDateItem.isDate()).isTrue(); 204 assertThat(firstDateItem.getDateTaken()).isNotEqualTo(0); 205 // Test the third item is date item and the dateTaken is larger than previous date item 206 final Item secondDateItem = itemList.get(2); 207 assertThat(secondDateItem.isDate()).isTrue(); 208 assertThat(secondDateItem.getDateTaken()).isGreaterThan(firstDateItem.getDateTaken()); 209 // Test the fifth item is date item and the dateTaken is larger than previous date item 210 final Item thirdDateItem = itemList.get(4); 211 assertThat(thirdDateItem.isDate()).isTrue(); 212 assertThat(thirdDateItem.getDateTaken()).isGreaterThan(secondDateItem.getDateTaken()); 213 } 214 215 @Test testGetCategoryItems_dataIsUpdated()216 public void testGetCategoryItems_dataIsUpdated() throws Exception { 217 final int itemCount = 3; 218 final LiveData<List<Item>> categoryItems = mPickerViewModel.getCategoryItems(FAKE_CATEGORY); 219 mItemsProvider.setItems(generateFakeImageItemList(itemCount)); 220 mPickerViewModel.updateCategoryItems(); 221 // We use ForegroundThread to execute the loadItems in updateCategoryItems(), wait for the 222 // thread idle 223 ForegroundThread.waitForIdle(); 224 225 final List<Item> itemList = categoryItems.getValue(); 226 227 // Original item count + 3 date items 228 assertThat(itemList.size()).isEqualTo(itemCount + 3); 229 230 final int updatedItemCount = 5; 231 mItemsProvider.setItems(generateFakeImageItemList(updatedItemCount)); 232 233 // trigger updateCategoryItems and wait the idle 234 mPickerViewModel.updateCategoryItems(); 235 236 // We use ForegroundThread to execute the loadItems in updateCategoryItems(), wait for the 237 // thread idle 238 ForegroundThread.waitForIdle(); 239 240 // Get the result again to check the result is as expected 241 final List<Item> updatedItemList = categoryItems.getValue(); 242 243 // Original item count + 5 date items 244 assertThat(updatedItemList.size()).isEqualTo(updatedItemCount + 5); 245 } 246 247 @Test testGetCategories()248 public void testGetCategories() throws Exception { 249 final Context context = InstrumentationRegistry.getTargetContext(); 250 final int categoryCount = 2; 251 try (final Cursor fakeCursor = generateCursorForFakeCategories(categoryCount)) { 252 fakeCursor.moveToFirst(); 253 final Category fakeFirstCategory = Category.fromCursor(fakeCursor, UserId.CURRENT_USER); 254 fakeCursor.moveToNext(); 255 final Category fakeSecondCategory = Category.fromCursor(fakeCursor, 256 UserId.CURRENT_USER); 257 mItemsProvider.setCategoriesCursor(fakeCursor); 258 // move the cursor to original position 259 fakeCursor.moveToPosition(-1); 260 mPickerViewModel.updateCategories(); 261 // We use ForegroundThread to execute the loadCategories in updateCategories(), wait for 262 // the thread idle 263 ForegroundThread.waitForIdle(); 264 265 final List<Category> categoryList = mPickerViewModel.getCategories().getValue(); 266 267 assertThat(categoryList.size()).isEqualTo(categoryCount); 268 // Verify the first category 269 final Category firstCategory = categoryList.get(0); 270 assertThat(firstCategory.getDisplayName(context)).isEqualTo( 271 fakeFirstCategory.getDisplayName(context)); 272 assertThat(firstCategory.getItemCount()).isEqualTo(fakeFirstCategory.getItemCount()); 273 assertThat(firstCategory.getCoverUri()).isEqualTo(fakeFirstCategory.getCoverUri()); 274 // Verify the second category 275 final Category secondCategory = categoryList.get(1); 276 assertThat(secondCategory.getDisplayName(context)).isEqualTo( 277 fakeSecondCategory.getDisplayName(context)); 278 assertThat(secondCategory.getItemCount()).isEqualTo(fakeSecondCategory.getItemCount()); 279 assertThat(secondCategory.getCoverUri()).isEqualTo(fakeSecondCategory.getCoverUri()); 280 } 281 } 282 283 generateFakeImageItem(String id)284 private static Item generateFakeImageItem(String id) { 285 final long dateTakenMs = System.currentTimeMillis() + Long.parseLong(id) 286 * DateUtils.DAY_IN_MILLIS; 287 final long generationModified = 1L; 288 289 return ItemTest.generateItem(id, FAKE_IMAGE_MIME_TYPE, dateTakenMs, generationModified, 290 /* duration= */ 1000L); 291 } 292 generateFakeImageItemList(int num)293 private static List<Item> generateFakeImageItemList(int num) { 294 final List<Item> itemList = new ArrayList<>(); 295 for (int i = 0; i < num; i++) { 296 itemList.add(generateFakeImageItem(String.valueOf(i))); 297 } 298 return itemList; 299 } 300 generateCursorForFakeCategories(int num)301 private static Cursor generateCursorForFakeCategories(int num) { 302 final MatrixCursor cursor = new MatrixCursor(AlbumColumns.ALL_PROJECTION); 303 final int itemCount = 5; 304 for (int i = 0; i < num; i++) { 305 cursor.addRow(new Object[]{ 306 FAKE_ID + String.valueOf(i), 307 System.currentTimeMillis(), 308 FAKE_CATEGORY_NAME + i, 309 FAKE_ID + String.valueOf(i), 310 itemCount + i, 311 PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY 312 }); 313 } 314 return cursor; 315 } 316 317 private static class TestItemsProvider extends ItemsProvider { 318 319 private List<Item> mItemList = new ArrayList<>(); 320 private Cursor mCategoriesCursor; 321 TestItemsProvider(Context context)322 public TestItemsProvider(Context context) { 323 super(context); 324 } 325 326 @Override getItems(Category category, int offset, int limit, @Nullable String mimeType, @Nullable UserId userId)327 public Cursor getItems(Category category, int offset, 328 int limit, @Nullable String mimeType, @Nullable UserId userId) throws 329 IllegalArgumentException, IllegalStateException { 330 final MatrixCursor c = new MatrixCursor(MediaColumns.ALL_PROJECTION); 331 332 for (Item item : mItemList) { 333 c.addRow(new String[] { 334 item.getId(), 335 String.valueOf(item.getDateTaken()), 336 String.valueOf(item.getGenerationModified()), 337 item.getMimeType(), 338 String.valueOf(item.getSpecialFormat()), 339 "1", // size_bytes 340 null, // media_store_uri 341 String.valueOf(item.getDuration()), 342 "0", // is_favorite 343 "/storage/emulated/0/foo", 344 PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY 345 }); 346 } 347 348 return c; 349 } 350 351 @Nullable getCategories(@ullable String mimeType, @Nullable UserId userId)352 public Cursor getCategories(@Nullable String mimeType, @Nullable UserId userId) { 353 if (mCategoriesCursor != null) { 354 return mCategoriesCursor; 355 } 356 357 final MatrixCursor c = new MatrixCursor(AlbumColumns.ALL_PROJECTION); 358 return c; 359 } 360 setItems(@onNull List<Item> itemList)361 public void setItems(@NonNull List<Item> itemList) { 362 mItemList = itemList; 363 } 364 setCategoriesCursor(@onNull Cursor cursor)365 public void setCategoriesCursor(@NonNull Cursor cursor) { 366 mCategoriesCursor = cursor; 367 } 368 } 369 370 @Test testParseValuesFromIntent_noMimeType_defaultFalse()371 public void testParseValuesFromIntent_noMimeType_defaultFalse() { 372 final Intent intent = new Intent(); 373 374 mPickerViewModel.parseValuesFromIntent(intent); 375 376 assertThat(mPickerViewModel.hasMimeTypeFilter()).isFalse(); 377 } 378 379 @Test testParseValuesFromIntent_validMimeType()380 public void testParseValuesFromIntent_validMimeType() { 381 final Intent intent = new Intent(); 382 intent.setType("image/png"); 383 384 mPickerViewModel.parseValuesFromIntent(intent); 385 386 assertThat(mPickerViewModel.hasMimeTypeFilter()).isTrue(); 387 } 388 389 @Test testParseValuesFromIntent_ignoreInvalidMimeType()390 public void testParseValuesFromIntent_ignoreInvalidMimeType() { 391 final Intent intent = new Intent(); 392 intent.setType("audio/*"); 393 394 mPickerViewModel.parseValuesFromIntent(intent); 395 396 assertThat(mPickerViewModel.hasMimeTypeFilter()).isFalse(); 397 } 398 } 399