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