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.junit.Assert.fail; 25 import static org.mockito.Mockito.mock; 26 import static org.mockito.Mockito.when; 27 28 import android.app.Application; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.database.Cursor; 32 import android.database.MatrixCursor; 33 import android.provider.MediaStore; 34 import android.text.format.DateUtils; 35 36 import androidx.annotation.NonNull; 37 import androidx.annotation.Nullable; 38 import androidx.test.InstrumentationRegistry; 39 import androidx.test.runner.AndroidJUnit4; 40 41 import com.android.providers.media.ConfigStore; 42 import com.android.providers.media.TestConfigStore; 43 import com.android.providers.media.photopicker.PickerSyncController; 44 import com.android.providers.media.photopicker.data.ItemsProvider; 45 import com.android.providers.media.photopicker.data.UserIdManager; 46 import com.android.providers.media.photopicker.data.model.Category; 47 import com.android.providers.media.photopicker.data.model.Item; 48 import com.android.providers.media.photopicker.data.model.ModelTestUtils; 49 import com.android.providers.media.photopicker.data.model.UserId; 50 import com.android.providers.media.util.ForegroundThread; 51 52 import org.junit.Before; 53 import org.junit.Rule; 54 import org.junit.Test; 55 import org.junit.runner.RunWith; 56 import org.mockito.Mock; 57 import org.mockito.MockitoAnnotations; 58 59 import java.util.ArrayList; 60 import java.util.List; 61 62 @RunWith(AndroidJUnit4.class) 63 public class PickerViewModelTest { 64 private static final String FAKE_CATEGORY_NAME = "testCategoryName"; 65 private static final String FAKE_ID = "5"; 66 67 @Rule 68 public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule(); 69 70 @Mock 71 private Application mApplication; 72 73 private PickerViewModel mPickerViewModel; 74 private TestItemsProvider mItemsProvider; 75 private TestConfigStore mConfigStore; 76 77 @Before setUp()78 public void setUp() { 79 MockitoAnnotations.initMocks(this); 80 81 final Context context = InstrumentationRegistry.getTargetContext(); 82 when(mApplication.getApplicationContext()).thenReturn(context); 83 mConfigStore = new TestConfigStore(); 84 mConfigStore.enableCloudMediaFeature(); 85 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 86 mPickerViewModel = new PickerViewModel(mApplication) { 87 @Override 88 protected ConfigStore getConfigStore() { 89 return mConfigStore; 90 } 91 }; 92 }); 93 mItemsProvider = new TestItemsProvider(context); 94 mPickerViewModel.setItemsProvider(mItemsProvider); 95 UserIdManager userIdManager = mock(UserIdManager.class); 96 when(userIdManager.getCurrentUserProfileId()).thenReturn(UserId.CURRENT_USER); 97 mPickerViewModel.setUserIdManager(userIdManager); 98 } 99 100 @Test testGetItems_noItems()101 public void testGetItems_noItems() { 102 final int itemCount = 0; 103 mItemsProvider.setItems(generateFakeImageItemList(itemCount)); 104 mPickerViewModel.updateItems(); 105 // We use ForegroundThread to execute the loadItems in updateItems(), wait for the thread 106 // idle 107 ForegroundThread.waitForIdle(); 108 109 final List<Item> itemList = mPickerViewModel.getItems().getValue(); 110 111 // No date headers, the size should be 0 112 assertThat(itemList.size()).isEqualTo(itemCount); 113 } 114 115 @Test testGetCategories()116 public void testGetCategories() throws Exception { 117 final Context context = InstrumentationRegistry.getTargetContext(); 118 final int categoryCount = 2; 119 try (final Cursor fakeCursor = generateCursorForFakeCategories(categoryCount)) { 120 fakeCursor.moveToFirst(); 121 final Category fakeFirstCategory = Category.fromCursor(fakeCursor, UserId.CURRENT_USER); 122 fakeCursor.moveToNext(); 123 final Category fakeSecondCategory = Category.fromCursor(fakeCursor, 124 UserId.CURRENT_USER); 125 mItemsProvider.setCategoriesCursor(fakeCursor); 126 // move the cursor to original position 127 fakeCursor.moveToPosition(-1); 128 mPickerViewModel.updateCategories(); 129 // We use ForegroundThread to execute the loadCategories in updateCategories(), wait for 130 // the thread idle 131 ForegroundThread.waitForIdle(); 132 133 final List<Category> categoryList = mPickerViewModel.getCategories().getValue(); 134 135 assertThat(categoryList.size()).isEqualTo(categoryCount); 136 // Verify the first category 137 final Category firstCategory = categoryList.get(0); 138 assertThat(firstCategory.getDisplayName(context)).isEqualTo( 139 fakeFirstCategory.getDisplayName(context)); 140 assertThat(firstCategory.getItemCount()).isEqualTo(fakeFirstCategory.getItemCount()); 141 assertThat(firstCategory.getCoverUri()).isEqualTo(fakeFirstCategory.getCoverUri()); 142 // Verify the second category 143 final Category secondCategory = categoryList.get(1); 144 assertThat(secondCategory.getDisplayName(context)).isEqualTo( 145 fakeSecondCategory.getDisplayName(context)); 146 assertThat(secondCategory.getItemCount()).isEqualTo(fakeSecondCategory.getItemCount()); 147 assertThat(secondCategory.getCoverUri()).isEqualTo(fakeSecondCategory.getCoverUri()); 148 } 149 } 150 generateFakeImageItem(String id)151 private static Item generateFakeImageItem(String id) { 152 final long dateTakenMs = System.currentTimeMillis() 153 + Long.parseLong(id) * DateUtils.DAY_IN_MILLIS; 154 return ModelTestUtils.generateJpegItem(id, dateTakenMs, /* generationModified */ 1L); 155 } 156 generateFakeImageItemList(int num)157 private static List<Item> generateFakeImageItemList(int num) { 158 final List<Item> itemList = new ArrayList<>(); 159 for (int i = 0; i < num; i++) { 160 itemList.add(generateFakeImageItem(String.valueOf(i))); 161 } 162 return itemList; 163 } 164 generateCursorForFakeCategories(int num)165 private static Cursor generateCursorForFakeCategories(int num) { 166 final MatrixCursor cursor = new MatrixCursor(AlbumColumns.ALL_PROJECTION); 167 final int itemCount = 5; 168 for (int i = 0; i < num; i++) { 169 cursor.addRow(new Object[]{ 170 FAKE_ID + String.valueOf(i), 171 System.currentTimeMillis(), 172 FAKE_CATEGORY_NAME + i, 173 FAKE_ID + String.valueOf(i), 174 itemCount + i, 175 PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY 176 }); 177 } 178 return cursor; 179 } 180 181 private static class TestItemsProvider extends ItemsProvider { 182 183 private List<Item> mItemList = new ArrayList<>(); 184 private Cursor mCategoriesCursor; 185 TestItemsProvider(Context context)186 public TestItemsProvider(Context context) { 187 super(context); 188 } 189 190 @Override getAllItems(Category category, int limit, @Nullable String[] mimeType, @Nullable UserId userId)191 public Cursor getAllItems(Category category, int limit, @Nullable String[] mimeType, 192 @Nullable UserId userId) throws 193 IllegalArgumentException, IllegalStateException { 194 final MatrixCursor c = new MatrixCursor(MediaColumns.ALL_PROJECTION); 195 196 for (Item item : mItemList) { 197 c.addRow(new String[] { 198 item.getId(), 199 String.valueOf(item.getDateTaken()), 200 String.valueOf(item.getGenerationModified()), 201 item.getMimeType(), 202 String.valueOf(item.getSpecialFormat()), 203 "1", // size_bytes 204 null, // media_store_uri 205 String.valueOf(item.getDuration()), 206 "0", // is_favorite 207 "/storage/emulated/0/foo", 208 PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY 209 }); 210 } 211 212 return c; 213 } 214 215 @Nullable getAllCategories(@ullable String[] mimeType, @Nullable UserId userId)216 public Cursor getAllCategories(@Nullable String[] mimeType, @Nullable UserId userId) { 217 if (mCategoriesCursor != null) { 218 return mCategoriesCursor; 219 } 220 221 final MatrixCursor c = new MatrixCursor(AlbumColumns.ALL_PROJECTION); 222 return c; 223 } 224 setItems(@onNull List<Item> itemList)225 public void setItems(@NonNull List<Item> itemList) { 226 mItemList = itemList; 227 } 228 setCategoriesCursor(@onNull Cursor cursor)229 public void setCategoriesCursor(@NonNull Cursor cursor) { 230 mCategoriesCursor = cursor; 231 } 232 } 233 234 @Test testParseValuesFromPickImagesIntent_noMimeType_defaultFalse()235 public void testParseValuesFromPickImagesIntent_noMimeType_defaultFalse() { 236 final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES); 237 238 mPickerViewModel.parseValuesFromIntent(intent); 239 240 assertThat(mPickerViewModel.hasMimeTypeFilters()).isFalse(); 241 } 242 243 @Test testParseValuesFromGetContentIntent_noMimeType_defaultFalse()244 public void testParseValuesFromGetContentIntent_noMimeType_defaultFalse() { 245 final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES); 246 intent.setType("*/*"); 247 248 mPickerViewModel.parseValuesFromIntent(intent); 249 250 assertThat(mPickerViewModel.hasMimeTypeFilters()).isFalse(); 251 } 252 253 @Test testParseValuesFromIntent_validMimeType()254 public void testParseValuesFromIntent_validMimeType() { 255 final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES); 256 intent.setType("image/png"); 257 258 mPickerViewModel.parseValuesFromIntent(intent); 259 260 assertThat(mPickerViewModel.hasMimeTypeFilters()).isTrue(); 261 } 262 263 @Test testParseValuesFromPickImagesIntent_validExtraMimeType()264 public void testParseValuesFromPickImagesIntent_validExtraMimeType() { 265 final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES); 266 intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"image/gif", "video/*"}); 267 268 mPickerViewModel.parseValuesFromIntent(intent); 269 270 assertThat(mPickerViewModel.hasMimeTypeFilters()).isTrue(); 271 } 272 273 @Test testParseValuesFromPickImagesIntent_invalidExtraMimeType()274 public void testParseValuesFromPickImagesIntent_invalidExtraMimeType() { 275 final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES); 276 intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"audio/*", "video/*"}); 277 278 try { 279 mPickerViewModel.parseValuesFromIntent(intent); 280 fail("Photo Picker does not support non-media mime type filters"); 281 } catch (IllegalArgumentException expected) { 282 // Expected 283 } 284 } 285 286 @Test testParseValuesFromPickImagesIntent_localOnlyTrue()287 public void testParseValuesFromPickImagesIntent_localOnlyTrue() { 288 final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES); 289 intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); 290 291 mPickerViewModel.parseValuesFromIntent(intent); 292 293 assertThat(mPickerViewModel.isLocalOnly()).isTrue(); 294 } 295 296 @Test testParseValuesFromPickImagesIntent_localOnlyFalse()297 public void testParseValuesFromPickImagesIntent_localOnlyFalse() { 298 final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES); 299 300 mPickerViewModel.parseValuesFromIntent(intent); 301 302 assertThat(mPickerViewModel.isLocalOnly()).isFalse(); 303 } 304 305 @Test testParseValuesFromGetContentIntent_validExtraMimeType()306 public void testParseValuesFromGetContentIntent_validExtraMimeType() { 307 final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 308 intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"image/gif", "video/*"}); 309 310 mPickerViewModel.parseValuesFromIntent(intent); 311 312 assertThat(mPickerViewModel.hasMimeTypeFilters()).isTrue(); 313 } 314 315 @Test testParseValuesFromGetContentIntent_invalidExtraMimeType()316 public void testParseValuesFromGetContentIntent_invalidExtraMimeType() { 317 final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 318 intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"audio/*", "video/*"}); 319 320 mPickerViewModel.parseValuesFromIntent(intent); 321 322 // non-media filters for GET_CONTENT show all images and videos 323 assertThat(mPickerViewModel.hasMimeTypeFilters()).isFalse(); 324 } 325 326 @Test testParseValuesFromGetContentIntent_localOnlyTrue()327 public void testParseValuesFromGetContentIntent_localOnlyTrue() { 328 final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 329 intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"video/*"}); 330 intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); 331 332 mPickerViewModel.parseValuesFromIntent(intent); 333 334 assertThat(mPickerViewModel.isLocalOnly()).isTrue(); 335 } 336 337 @Test testParseValuesFromGetContentIntent_localOnlyFalse()338 public void testParseValuesFromGetContentIntent_localOnlyFalse() { 339 final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 340 intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"video/*"}); 341 342 mPickerViewModel.parseValuesFromIntent(intent); 343 344 assertThat(mPickerViewModel.isLocalOnly()).isFalse(); 345 } 346 347 @Test testShouldShowOnlyLocalFeatures()348 public void testShouldShowOnlyLocalFeatures() { 349 mConfigStore.enableCloudMediaFeature(); 350 351 Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES); 352 intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); 353 mPickerViewModel.parseValuesFromIntent(intent); 354 assertThat(mPickerViewModel.isLocalOnly()).isTrue(); 355 assertThat(mPickerViewModel.shouldShowOnlyLocalFeatures()).isTrue(); 356 357 intent.removeExtra(Intent.EXTRA_LOCAL_ONLY); 358 mPickerViewModel.parseValuesFromIntent(intent); 359 assertThat(mPickerViewModel.isLocalOnly()).isFalse(); 360 assertThat(mPickerViewModel.shouldShowOnlyLocalFeatures()).isFalse(); 361 362 mConfigStore.disableCloudMediaFeature(); 363 assertThat(mPickerViewModel.shouldShowOnlyLocalFeatures()).isTrue(); 364 } 365 } 366