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