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