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