1 /* 2 * Copyright (C) 2011 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.contacts; 18 19 import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY; 20 21 import android.database.Cursor; 22 import android.database.sqlite.SQLiteDatabase; 23 import android.provider.ContactsContract; 24 import android.provider.ContactsContract.PhotoFiles; 25 26 import androidx.test.filters.MediumTest; 27 28 import com.android.providers.contacts.ContactsDatabaseHelper.Tables; 29 import com.android.providers.contacts.tests.R; 30 31 import java.io.File; 32 import java.io.FileInputStream; 33 import java.io.IOException; 34 import java.util.HashMap; 35 import java.util.HashSet; 36 import java.util.Map; 37 import java.util.Set; 38 39 /** 40 * Tests for {@link PhotoStore}. 41 */ 42 @MediumTest 43 public class PhotoStoreTest extends PhotoLoadingTestCase { 44 45 private ContactsActor mActor; 46 private SynchronousContactsProvider2 mProvider; 47 private SQLiteDatabase mDb; 48 49 // The object under test. 50 private PhotoStore mPhotoStore; 51 52 @Override setUp()53 protected void setUp() throws Exception { 54 super.setUp(); 55 mActor = new ContactsActor(getContext(), PACKAGE_GREY, SynchronousContactsProvider2.class, 56 ContactsContract.AUTHORITY); 57 mProvider = ((SynchronousContactsProvider2) mActor.provider); 58 mPhotoStore = mProvider.getPhotoStore(); 59 mProvider.wipeData(); 60 mDb = mProvider.getDatabaseHelper().getReadableDatabase(); 61 } 62 63 @Override tearDown()64 protected void tearDown() throws Exception { 65 super.tearDown(); 66 mPhotoStore.clear(); 67 } 68 testStoreThumbnailPhoto()69 public void testStoreThumbnailPhoto() throws IOException { 70 byte[] photo = loadPhotoFromResource(R.drawable.earth_small, PhotoSize.ORIGINAL); 71 72 // Since the photo is already thumbnail-sized, no file will be stored. 73 assertEquals(0, mPhotoStore.insert(newPhotoProcessor(photo, false))); 74 } 75 testStore200Photo()76 public void testStore200Photo() throws IOException { 77 // As 200 is below the full photo size, we don't want to see it upscaled 78 runStorageTestForResource(R.drawable.earth_200, 200, 200); 79 } 80 testStoreNonSquare300x200Photo()81 public void testStoreNonSquare300x200Photo() throws IOException { 82 // The longer side should be downscaled to the target size 83 runStorageTestForResource(R.drawable.earth_300x200, 256, 170); 84 } 85 testStoreNonSquare300x200PhotoWithCrop()86 public void testStoreNonSquare300x200PhotoWithCrop() throws IOException { 87 // As 300x200 is below the full photo size, we don't want to see it upscaled 88 // This one is not square, so we expect the longer side to be cropped 89 runStorageTestForResourceWithCrop(R.drawable.earth_300x200, 200, 200); 90 } 91 testStoreNonSquare600x400PhotoWithCrop()92 public void testStoreNonSquare600x400PhotoWithCrop() throws IOException { 93 // As 600x400 is above the full photo size, we expect the picture to be cropped and then 94 // scaled 95 runStorageTestForResourceWithCrop(R.drawable.earth_600x400, 256, 256); 96 } 97 testStoreMediumPhoto()98 public void testStoreMediumPhoto() throws IOException { 99 // Source Image is 256x256 100 runStorageTestForResource(R.drawable.earth_normal, 256, 256); 101 } 102 testStoreLargePhoto()103 public void testStoreLargePhoto() throws IOException { 104 // Source image is 512x512 105 runStorageTestForResource(R.drawable.earth_large, 256, 256); 106 } 107 testStoreHugePhoto()108 public void testStoreHugePhoto() throws IOException { 109 // Source image is 1024x1024 110 runStorageTestForResource(R.drawable.earth_huge, 256, 256); 111 } 112 113 /** 114 * Runs the following steps: 115 * - Loads the given photo resource. 116 * - Inserts it into the photo store. 117 * - Checks that the photo has a photo file ID. 118 * - Loads the expected display photo for the resource. 119 * - Gets the photo entry from the photo store. 120 * - Loads the photo entry's file content from disk. 121 * - Compares the expected photo content to the disk content. 122 * - Queries the contacts provider for the photo file entry, checks for its 123 * existence, and matches it up against the expected metadata. 124 * - Checks that the total storage taken up by the photo store is equal to 125 * the size of the photo. 126 * @param resourceId The resource ID of the photo file to test. 127 */ runStorageTestForResource(int resourceId, int expectedWidth, int expectedHeight)128 public void runStorageTestForResource(int resourceId, int expectedWidth, 129 int expectedHeight) throws IOException { 130 byte[] photo = loadPhotoFromResource(resourceId, PhotoSize.ORIGINAL); 131 long photoFileId = mPhotoStore.insert(newPhotoProcessor(photo, false)); 132 assertTrue(photoFileId != 0); 133 134 File storedFile = new File(mPhotoStore.get(photoFileId).path); 135 assertTrue(storedFile.exists()); 136 byte[] actualStoredVersion = readInputStreamFully(new FileInputStream(storedFile)); 137 138 byte[] expectedStoredVersion = loadPhotoFromResource(resourceId, PhotoSize.DISPLAY_PHOTO); 139 140 EvenMoreAsserts.assertImageRawData(getContext(), 141 expectedStoredVersion, actualStoredVersion); 142 143 Cursor c = mDb.query(Tables.PHOTO_FILES, 144 new String[]{PhotoFiles.WIDTH, PhotoFiles.HEIGHT, PhotoFiles.FILESIZE}, 145 PhotoFiles._ID + "=?", new String[]{String.valueOf(photoFileId)}, null, null, null); 146 try { 147 assertEquals(1, c.getCount()); 148 c.moveToFirst(); 149 assertEquals(expectedWidth + "/" + expectedHeight, c.getInt(0) + "/" + c.getInt(1)); 150 assertEquals(expectedStoredVersion.length, c.getInt(2)); 151 } finally { 152 c.close(); 153 } 154 155 assertEquals(expectedStoredVersion.length, mPhotoStore.getTotalSize()); 156 } 157 runStorageTestForResourceWithCrop(int resourceId, int expectedWidth, int expectedHeight)158 public void runStorageTestForResourceWithCrop(int resourceId, int expectedWidth, 159 int expectedHeight) throws IOException { 160 byte[] photo = loadPhotoFromResource(resourceId, PhotoSize.ORIGINAL); 161 long photoFileId = mPhotoStore.insert(newPhotoProcessor(photo, true)); 162 assertTrue(photoFileId != 0); 163 164 Cursor c = mDb.query(Tables.PHOTO_FILES, 165 new String[]{PhotoFiles.HEIGHT, PhotoFiles.WIDTH, PhotoFiles.FILESIZE}, 166 PhotoFiles._ID + "=?", new String[]{String.valueOf(photoFileId)}, null, null, null); 167 try { 168 assertEquals(1, c.getCount()); 169 c.moveToFirst(); 170 assertEquals(expectedWidth + "/" + expectedHeight, c.getInt(0) + "/" + c.getInt(1)); 171 } finally { 172 c.close(); 173 } 174 } 175 testRemoveEntry()176 public void testRemoveEntry() throws IOException { 177 byte[] photo = loadPhotoFromResource(R.drawable.earth_normal, PhotoSize.ORIGINAL); 178 long photoFileId = mPhotoStore.insert(newPhotoProcessor(photo, false)); 179 PhotoStore.Entry entry = mPhotoStore.get(photoFileId); 180 assertTrue(new File(entry.path).exists()); 181 182 mPhotoStore.remove(photoFileId); 183 184 // Check that the file has been deleted. 185 assertFalse(new File(entry.path).exists()); 186 187 // Check that the database record has also been removed. 188 Cursor c = mDb.query(Tables.PHOTO_FILES, new String[]{PhotoFiles._ID}, 189 PhotoFiles._ID + "=?", new String[]{String.valueOf(photoFileId)}, null, null, null); 190 try { 191 assertEquals(0, c.getCount()); 192 } finally { 193 c.close(); 194 } 195 } 196 testCleanup()197 public void testCleanup() throws IOException { 198 // Load some photos into the store. 199 Set<Long> photoFileIds = new HashSet<Long>(); 200 Map<Integer, Long> resourceIdToPhotoMap = new HashMap<Integer, Long>(); 201 int[] resourceIds = new int[] { 202 R.drawable.earth_normal, R.drawable.earth_large, R.drawable.earth_huge 203 }; 204 for (int resourceId : resourceIds) { 205 long photoFileId = mPhotoStore.insert( 206 new PhotoProcessor(loadPhotoFromResource(resourceId, PhotoSize.ORIGINAL), 207 256, 96)); 208 resourceIdToPhotoMap.put(resourceId, photoFileId); 209 photoFileIds.add(photoFileId); 210 } 211 assertFalse(photoFileIds.contains(0L)); 212 assertEquals(3, photoFileIds.size()); 213 214 // Run cleanup with the indication that only the large and huge photos are in use, along 215 // with a bogus photo file ID that isn't in the photo store. 216 long bogusPhotoFileId = 123456789; 217 Set<Long> photoFileIdsInUse = new HashSet<Long>(); 218 photoFileIdsInUse.add(resourceIdToPhotoMap.get(R.drawable.earth_large)); 219 photoFileIdsInUse.add(resourceIdToPhotoMap.get(R.drawable.earth_huge)); 220 photoFileIdsInUse.add(bogusPhotoFileId); 221 222 Set<Long> photoIdsToCleanup = mPhotoStore.cleanup(photoFileIdsInUse); 223 224 // The set of photo IDs to clean up should consist of the bogus photo file ID. 225 assertEquals(1, photoIdsToCleanup.size()); 226 assertTrue(photoIdsToCleanup.contains(bogusPhotoFileId)); 227 228 // The entry for the normal-sized photo should have been cleaned up, since it isn't being 229 // used. 230 long normalPhotoId = resourceIdToPhotoMap.get(R.drawable.earth_normal); 231 assertNull(mPhotoStore.get(normalPhotoId)); 232 233 // Check that the database record has also been removed. 234 Cursor c = mDb.query(Tables.PHOTO_FILES, new String[]{PhotoFiles._ID}, 235 PhotoFiles._ID + "=?", new String[]{String.valueOf(normalPhotoId)}, 236 null, null, null); 237 try { 238 assertEquals(0, c.getCount()); 239 } finally { 240 c.close(); 241 } 242 } 243 } 244