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