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