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