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
17package com.android.providers.contacts;
18
19import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY;
20
21import android.database.Cursor;
22import android.database.sqlite.SQLiteDatabase;
23import android.provider.ContactsContract;
24import android.provider.ContactsContract.PhotoFiles;
25import android.test.suitebuilder.annotation.MediumTest;
26
27import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
28import com.android.providers.contacts.tests.R;
29
30import java.io.File;
31import java.io.FileInputStream;
32import java.io.IOException;
33import java.util.HashMap;
34import java.util.HashSet;
35import java.util.Map;
36import java.util.Set;
37
38/**
39 * Tests for {@link PhotoStore}.
40 */
41@MediumTest
42public 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
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(getContext()).getReadableDatabase();
60    }
61
62    @Override
63    protected void tearDown() throws Exception {
64        super.tearDown();
65        mPhotoStore.clear();
66    }
67
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
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
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
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
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
97    public void testStoreMediumPhoto() throws IOException {
98        // Source Image is 256x256
99        runStorageTestForResource(R.drawable.earth_normal, 256, 256);
100    }
101
102    public void testStoreLargePhoto() throws IOException {
103        // Source image is 512x512
104        runStorageTestForResource(R.drawable.earth_large, 256, 256);
105    }
106
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     */
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
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
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
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