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