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