LocalImage.java revision 4b4dbd225685502f4249c2bf25bf74f7ce526645
1/*
2 * Copyright (C) 2010 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.gallery3d.data;
18
19import android.annotation.TargetApi;
20import android.content.ContentResolver;
21import android.content.ContentValues;
22import android.database.Cursor;
23import android.graphics.Bitmap;
24import android.graphics.BitmapFactory;
25import android.graphics.BitmapRegionDecoder;
26import android.media.ExifInterface;
27import android.net.Uri;
28import android.os.Build;
29import android.provider.MediaStore.Images;
30import android.provider.MediaStore.Images.ImageColumns;
31import android.provider.MediaStore.MediaColumns;
32import android.util.Log;
33
34import com.android.gallery3d.app.GalleryApp;
35import com.android.gallery3d.app.PanoramaMetadataSupport;
36import com.android.gallery3d.app.StitchingProgressManager;
37import com.android.gallery3d.common.ApiHelper;
38import com.android.gallery3d.common.BitmapUtils;
39import com.android.gallery3d.util.GalleryUtils;
40import com.android.gallery3d.util.ThreadPool.Job;
41import com.android.gallery3d.util.ThreadPool.JobContext;
42import com.android.gallery3d.util.UpdateHelper;
43
44import java.io.File;
45import java.io.IOException;
46
47// LocalImage represents an image in the local storage.
48public class LocalImage extends LocalMediaItem {
49    private static final String TAG = "LocalImage";
50
51    static final Path ITEM_PATH = Path.fromString("/local/image/item");
52
53    // Must preserve order between these indices and the order of the terms in
54    // the following PROJECTION array.
55    private static final int INDEX_ID = 0;
56    private static final int INDEX_CAPTION = 1;
57    private static final int INDEX_MIME_TYPE = 2;
58    private static final int INDEX_LATITUDE = 3;
59    private static final int INDEX_LONGITUDE = 4;
60    private static final int INDEX_DATE_TAKEN = 5;
61    private static final int INDEX_DATE_ADDED = 6;
62    private static final int INDEX_DATE_MODIFIED = 7;
63    private static final int INDEX_DATA = 8;
64    private static final int INDEX_ORIENTATION = 9;
65    private static final int INDEX_BUCKET_ID = 10;
66    private static final int INDEX_SIZE = 11;
67    private static final int INDEX_WIDTH = 12;
68    private static final int INDEX_HEIGHT = 13;
69
70    static final String[] PROJECTION =  {
71            ImageColumns._ID,           // 0
72            ImageColumns.TITLE,         // 1
73            ImageColumns.MIME_TYPE,     // 2
74            ImageColumns.LATITUDE,      // 3
75            ImageColumns.LONGITUDE,     // 4
76            ImageColumns.DATE_TAKEN,    // 5
77            ImageColumns.DATE_ADDED,    // 6
78            ImageColumns.DATE_MODIFIED, // 7
79            ImageColumns.DATA,          // 8
80            ImageColumns.ORIENTATION,   // 9
81            ImageColumns.BUCKET_ID,     // 10
82            ImageColumns.SIZE,          // 11
83            "0",                        // 12
84            "0"                         // 13
85    };
86
87    static {
88        updateWidthAndHeightProjection();
89    }
90
91    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
92    private static void updateWidthAndHeightProjection() {
93        if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
94            PROJECTION[INDEX_WIDTH] = MediaColumns.WIDTH;
95            PROJECTION[INDEX_HEIGHT] = MediaColumns.HEIGHT;
96        }
97    }
98
99    private final GalleryApp mApplication;
100
101    public int rotation;
102
103    private PanoramaMetadataSupport mPanoramaMetadata = new PanoramaMetadataSupport(this);
104
105    public LocalImage(Path path, GalleryApp application, Cursor cursor) {
106        super(path, nextVersionNumber());
107        mApplication = application;
108        loadFromCursor(cursor);
109    }
110
111    public LocalImage(Path path, GalleryApp application, int id) {
112        super(path, nextVersionNumber());
113        mApplication = application;
114        ContentResolver resolver = mApplication.getContentResolver();
115        Uri uri = Images.Media.EXTERNAL_CONTENT_URI;
116        Cursor cursor = LocalAlbum.getItemCursor(resolver, uri, PROJECTION, id);
117        if (cursor == null) {
118            throw new RuntimeException("cannot get cursor for: " + path);
119        }
120        try {
121            if (cursor.moveToNext()) {
122                loadFromCursor(cursor);
123            } else {
124                throw new RuntimeException("cannot find data for: " + path);
125            }
126        } finally {
127            cursor.close();
128        }
129    }
130
131    private void loadFromCursor(Cursor cursor) {
132        id = cursor.getInt(INDEX_ID);
133        caption = cursor.getString(INDEX_CAPTION);
134        mimeType = cursor.getString(INDEX_MIME_TYPE);
135        latitude = cursor.getDouble(INDEX_LATITUDE);
136        longitude = cursor.getDouble(INDEX_LONGITUDE);
137        dateTakenInMs = cursor.getLong(INDEX_DATE_TAKEN);
138        dateAddedInSec = cursor.getLong(INDEX_DATE_ADDED);
139        dateModifiedInSec = cursor.getLong(INDEX_DATE_MODIFIED);
140        filePath = cursor.getString(INDEX_DATA);
141        rotation = cursor.getInt(INDEX_ORIENTATION);
142        bucketId = cursor.getInt(INDEX_BUCKET_ID);
143        fileSize = cursor.getLong(INDEX_SIZE);
144        width = cursor.getInt(INDEX_WIDTH);
145        height = cursor.getInt(INDEX_HEIGHT);
146    }
147
148    @Override
149    protected boolean updateFromCursor(Cursor cursor) {
150        UpdateHelper uh = new UpdateHelper();
151        id = uh.update(id, cursor.getInt(INDEX_ID));
152        caption = uh.update(caption, cursor.getString(INDEX_CAPTION));
153        mimeType = uh.update(mimeType, cursor.getString(INDEX_MIME_TYPE));
154        latitude = uh.update(latitude, cursor.getDouble(INDEX_LATITUDE));
155        longitude = uh.update(longitude, cursor.getDouble(INDEX_LONGITUDE));
156        dateTakenInMs = uh.update(
157                dateTakenInMs, cursor.getLong(INDEX_DATE_TAKEN));
158        dateAddedInSec = uh.update(
159                dateAddedInSec, cursor.getLong(INDEX_DATE_ADDED));
160        dateModifiedInSec = uh.update(
161                dateModifiedInSec, cursor.getLong(INDEX_DATE_MODIFIED));
162        filePath = uh.update(filePath, cursor.getString(INDEX_DATA));
163        rotation = uh.update(rotation, cursor.getInt(INDEX_ORIENTATION));
164        bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID));
165        fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE));
166        width = uh.update(width, cursor.getInt(INDEX_WIDTH));
167        height = uh.update(height, cursor.getInt(INDEX_HEIGHT));
168        return uh.isUpdated();
169    }
170
171    @Override
172    public Job<Bitmap> requestImage(int type) {
173        return new LocalImageRequest(mApplication, mPath, type, filePath);
174    }
175
176    public static class LocalImageRequest extends ImageCacheRequest {
177        private String mLocalFilePath;
178
179        LocalImageRequest(GalleryApp application, Path path, int type,
180                String localFilePath) {
181            super(application, path, type, MediaItem.getTargetSize(type));
182            mLocalFilePath = localFilePath;
183        }
184
185        @Override
186        public Bitmap onDecodeOriginal(JobContext jc, final int type) {
187            BitmapFactory.Options options = new BitmapFactory.Options();
188            options.inPreferredConfig = Bitmap.Config.ARGB_8888;
189            int targetSize = MediaItem.getTargetSize(type);
190
191            // try to decode from JPEG EXIF
192            if (type == MediaItem.TYPE_MICROTHUMBNAIL) {
193                ExifInterface exif = null;
194                byte [] thumbData = null;
195                try {
196                    exif = new ExifInterface(mLocalFilePath);
197                    if (exif != null) {
198                        thumbData = exif.getThumbnail();
199                    }
200                } catch (Throwable t) {
201                    Log.w(TAG, "fail to get exif thumb", t);
202                }
203                if (thumbData != null) {
204                    Bitmap bitmap = DecodeUtils.decodeIfBigEnough(
205                            jc, thumbData, options, targetSize);
206                    if (bitmap != null) return bitmap;
207                }
208            }
209
210            return DecodeUtils.decodeThumbnail(jc, mLocalFilePath, options, targetSize, type);
211        }
212    }
213
214    @Override
215    public Job<BitmapRegionDecoder> requestLargeImage() {
216        return new LocalLargeImageRequest(filePath);
217    }
218
219    public static class LocalLargeImageRequest
220            implements Job<BitmapRegionDecoder> {
221        String mLocalFilePath;
222
223        public LocalLargeImageRequest(String localFilePath) {
224            mLocalFilePath = localFilePath;
225        }
226
227        @Override
228        public BitmapRegionDecoder run(JobContext jc) {
229            return DecodeUtils.createBitmapRegionDecoder(jc, mLocalFilePath, false);
230        }
231    }
232
233    @Override
234    public int getSupportedOperations() {
235        StitchingProgressManager progressManager = mApplication.getStitchingProgressManager();
236        if (progressManager != null && progressManager.getProgress(getContentUri()) != null) {
237            return 0; // doesn't support anything while stitching!
238        }
239        int operation = SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_CROP
240                | SUPPORT_SETAS | SUPPORT_EDIT | SUPPORT_INFO;
241        if (BitmapUtils.isSupportedByRegionDecoder(mimeType)) {
242            operation |= SUPPORT_FULL_IMAGE;
243        }
244
245        if (BitmapUtils.isRotationSupported(mimeType)) {
246            operation |= SUPPORT_ROTATE;
247        }
248
249        if (GalleryUtils.isValidLocation(latitude, longitude)) {
250            operation |= SUPPORT_SHOW_ON_MAP;
251        }
252        return operation;
253    }
254
255    @Override
256    public void getPanoramaSupport(PanoramaSupportCallback callback) {
257        mPanoramaMetadata.getPanoramaSupport(mApplication, callback);
258    }
259
260    @Override
261    public void clearCachedPanoramaSupport() {
262        mPanoramaMetadata.clearCachedValues();
263    }
264
265    @Override
266    public void delete() {
267        GalleryUtils.assertNotInRenderThread();
268        Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
269        mApplication.getContentResolver().delete(baseUri, "_id=?",
270                new String[]{String.valueOf(id)});
271    }
272
273    private static String getExifOrientation(int orientation) {
274        switch (orientation) {
275            case 0:
276                return String.valueOf(ExifInterface.ORIENTATION_NORMAL);
277            case 90:
278                return String.valueOf(ExifInterface.ORIENTATION_ROTATE_90);
279            case 180:
280                return String.valueOf(ExifInterface.ORIENTATION_ROTATE_180);
281            case 270:
282                return String.valueOf(ExifInterface.ORIENTATION_ROTATE_270);
283            default:
284                throw new AssertionError("invalid: " + orientation);
285        }
286    }
287
288    @Override
289    public void rotate(int degrees) {
290        GalleryUtils.assertNotInRenderThread();
291        Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
292        ContentValues values = new ContentValues();
293        int rotation = (this.rotation + degrees) % 360;
294        if (rotation < 0) rotation += 360;
295
296        if (mimeType.equalsIgnoreCase("image/jpeg")) {
297            try {
298                ExifInterface exif = new ExifInterface(filePath);
299                exif.setAttribute(ExifInterface.TAG_ORIENTATION,
300                        getExifOrientation(rotation));
301                exif.saveAttributes();
302            } catch (IOException e) {
303                Log.w(TAG, "cannot set exif data: " + filePath);
304            }
305
306            // We need to update the filesize as well
307            fileSize = new File(filePath).length();
308            values.put(Images.Media.SIZE, fileSize);
309        }
310
311        values.put(Images.Media.ORIENTATION, rotation);
312        mApplication.getContentResolver().update(baseUri, values, "_id=?",
313                new String[]{String.valueOf(id)});
314    }
315
316    @Override
317    public Uri getContentUri() {
318        Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
319        return baseUri.buildUpon().appendPath(String.valueOf(id)).build();
320    }
321
322    @Override
323    public int getMediaType() {
324        return MEDIA_TYPE_IMAGE;
325    }
326
327    @Override
328    public MediaDetails getDetails() {
329        MediaDetails details = super.getDetails();
330        details.addDetail(MediaDetails.INDEX_ORIENTATION, Integer.valueOf(rotation));
331        if (MIME_TYPE_JPEG.equals(mimeType)) {
332            // ExifInterface returns incorrect values for photos in other format.
333            // For example, the width and height of an webp images is always '0'.
334            MediaDetails.extractExifInfo(details, filePath);
335        }
336        return details;
337    }
338
339    @Override
340    public int getRotation() {
341        return rotation;
342    }
343
344    @Override
345    public int getWidth() {
346        return width;
347    }
348
349    @Override
350    public int getHeight() {
351        return height;
352    }
353
354    @Override
355    public String getFilePath() {
356        return filePath;
357    }
358}
359