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 
17 package com.android.gallery3d.data;
18 
19 import android.annotation.TargetApi;
20 import android.content.ContentResolver;
21 import android.content.ContentValues;
22 import android.database.Cursor;
23 import android.graphics.Bitmap;
24 import android.graphics.BitmapFactory;
25 import android.graphics.BitmapRegionDecoder;
26 import android.net.Uri;
27 import android.os.Build;
28 import android.provider.MediaStore.Images;
29 import android.provider.MediaStore.Images.ImageColumns;
30 import android.provider.MediaStore.MediaColumns;
31 import android.util.Log;
32 
33 import com.android.gallery3d.app.GalleryApp;
34 import com.android.gallery3d.app.PanoramaMetadataSupport;
35 import com.android.gallery3d.common.ApiHelper;
36 import com.android.gallery3d.common.BitmapUtils;
37 import com.android.gallery3d.exif.ExifInterface;
38 import com.android.gallery3d.exif.ExifTag;
39 import com.android.gallery3d.filtershow.tools.SaveImage;
40 import com.android.gallery3d.util.GalleryUtils;
41 import com.android.gallery3d.util.ThreadPool.Job;
42 import com.android.gallery3d.util.ThreadPool.JobContext;
43 import com.android.gallery3d.util.UpdateHelper;
44 
45 import java.io.File;
46 import java.io.FileNotFoundException;
47 import java.io.IOException;
48 
49 // LocalImage represents an image in the local storage.
50 public 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 {
updateWidthAndHeightProjection()90         updateWidthAndHeightProjection();
91     }
92 
93     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
updateWidthAndHeightProjection()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 
LocalImage(Path path, GalleryApp application, Cursor cursor)107     public LocalImage(Path path, GalleryApp application, Cursor cursor) {
108         super(path, nextVersionNumber());
109         mApplication = application;
110         loadFromCursor(cursor);
111     }
112 
LocalImage(Path path, GalleryApp application, int id)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 
loadFromCursor(Cursor cursor)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
updateFromCursor(Cursor cursor)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
requestImage(int type)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 
LocalImageRequest(GalleryApp application, Path path, long timeModified, int type, String localFilePath)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
onDecodeOriginal(JobContext jc, final int type)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
requestLargeImage()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 
LocalLargeImageRequest(String localFilePath)227         public LocalLargeImageRequest(String localFilePath) {
228             mLocalFilePath = localFilePath;
229         }
230 
231         @Override
run(JobContext jc)232         public BitmapRegionDecoder run(JobContext jc) {
233             return DecodeUtils.createBitmapRegionDecoder(jc, mLocalFilePath, false);
234         }
235     }
236 
237     @Override
getSupportedOperations()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
getPanoramaSupport(PanoramaSupportCallback callback)256     public void getPanoramaSupport(PanoramaSupportCallback callback) {
257         mPanoramaMetadata.getPanoramaSupport(mApplication, callback);
258     }
259 
260     @Override
clearCachedPanoramaSupport()261     public void clearCachedPanoramaSupport() {
262         mPanoramaMetadata.clearCachedValues();
263     }
264 
265     @Override
delete()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
rotate(int degrees)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
getContentUri()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
getMediaType()315     public int getMediaType() {
316         return MEDIA_TYPE_IMAGE;
317     }
318 
319     @Override
getDetails()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
getRotation()332     public int getRotation() {
333         return rotation;
334     }
335 
336     @Override
getWidth()337     public int getWidth() {
338         return width;
339     }
340 
341     @Override
getHeight()342     public int getHeight() {
343         return height;
344     }
345 
346     @Override
getFilePath()347     public String getFilePath() {
348         return filePath;
349     }
350 }
351