Home | History | Annotate | Download | only in gallery
      1 /*
      2  * Copyright (C) 2009 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.camera.gallery;
     18 
     19 import com.android.camera.ImageManager;
     20 import com.android.camera.Util;
     21 
     22 import android.content.ContentResolver;
     23 import android.content.ContentUris;
     24 import android.database.Cursor;
     25 import android.net.Uri;
     26 import android.util.Log;
     27 
     28 import java.util.regex.Matcher;
     29 import java.util.regex.Pattern;
     30 
     31 /**
     32  * A collection of <code>BaseImage</code>s.
     33  */
     34 public abstract class BaseImageList implements IImageList {
     35     private static final String TAG = "BaseImageList";
     36     private static final int CACHE_CAPACITY = 512;
     37     private final LruCache<Integer, BaseImage> mCache =
     38             new LruCache<Integer, BaseImage>(CACHE_CAPACITY);
     39 
     40     protected ContentResolver mContentResolver;
     41     protected int mSort;
     42 
     43     protected Uri mBaseUri;
     44     protected Cursor mCursor;
     45     protected String mBucketId;
     46     protected boolean mCursorDeactivated = false;
     47 
     48     public BaseImageList(ContentResolver resolver, Uri uri, int sort,
     49             String bucketId) {
     50         mSort = sort;
     51         mBaseUri = uri;
     52         mBucketId = bucketId;
     53         mContentResolver = resolver;
     54         mCursor = createCursor();
     55 
     56         if (mCursor == null) {
     57             Log.w(TAG, "createCursor returns null.");
     58         }
     59 
     60         // TODO: We need to clear the cache because we may "reopen" the image
     61         // list. After we implement the image list state, we can remove this
     62         // kind of usage.
     63         mCache.clear();
     64     }
     65 
     66     public void close() {
     67         try {
     68             invalidateCursor();
     69         } catch (IllegalStateException e) {
     70             // IllegalStateException may be thrown if the cursor is stale.
     71             Log.e(TAG, "Caught exception while deactivating cursor.", e);
     72         }
     73         mContentResolver = null;
     74         if (mCursor != null) {
     75             mCursor.close();
     76             mCursor = null;
     77         }
     78     }
     79 
     80     // TODO: Change public to protected
     81     public Uri contentUri(long id) {
     82         // TODO: avoid using exception for most cases
     83         try {
     84             // does our uri already have an id (single image query)?
     85             // if so just return it
     86             long existingId = ContentUris.parseId(mBaseUri);
     87             if (existingId != id) Log.e(TAG, "id mismatch");
     88             return mBaseUri;
     89         } catch (NumberFormatException ex) {
     90             // otherwise tack on the id
     91             return ContentUris.withAppendedId(mBaseUri, id);
     92         }
     93     }
     94 
     95     public int getCount() {
     96         Cursor cursor = getCursor();
     97         if (cursor == null) return 0;
     98         synchronized (this) {
     99             return cursor.getCount();
    100         }
    101     }
    102 
    103     public boolean isEmpty() {
    104         return getCount() == 0;
    105     }
    106 
    107     private Cursor getCursor() {
    108         synchronized (this) {
    109             if (mCursor == null) return null;
    110             if (mCursorDeactivated) {
    111                 mCursor.requery();
    112                 mCursorDeactivated = false;
    113             }
    114             return mCursor;
    115         }
    116     }
    117 
    118     public IImage getImageAt(int i) {
    119         BaseImage result = mCache.get(i);
    120         if (result == null) {
    121             Cursor cursor = getCursor();
    122             if (cursor == null) return null;
    123             synchronized (this) {
    124                 result = cursor.moveToPosition(i)
    125                         ? loadImageFromCursor(cursor)
    126                         : null;
    127                 mCache.put(i, result);
    128             }
    129         }
    130         return result;
    131     }
    132 
    133     public boolean removeImage(IImage image) {
    134         // TODO: need to delete the thumbnails as well
    135         if (mContentResolver.delete(image.fullSizeImageUri(), null, null) > 0) {
    136             ((BaseImage) image).onRemove();
    137             invalidateCursor();
    138             invalidateCache();
    139             return true;
    140         } else {
    141             return false;
    142         }
    143     }
    144 
    145     public boolean removeImageAt(int i) {
    146         // TODO: need to delete the thumbnails as well
    147         return removeImage(getImageAt(i));
    148     }
    149 
    150     protected abstract Cursor createCursor();
    151 
    152     protected abstract BaseImage loadImageFromCursor(Cursor cursor);
    153 
    154     protected abstract long getImageId(Cursor cursor);
    155 
    156     protected void invalidateCursor() {
    157         if (mCursor == null) return;
    158         mCursor.deactivate();
    159         mCursorDeactivated = true;
    160     }
    161 
    162     protected void invalidateCache() {
    163         mCache.clear();
    164     }
    165 
    166     private static final Pattern sPathWithId = Pattern.compile("(.*)/\\d+");
    167 
    168     private static String getPathWithoutId(Uri uri) {
    169         String path = uri.getPath();
    170         Matcher matcher = sPathWithId.matcher(path);
    171         return matcher.matches() ? matcher.group(1) : path;
    172     }
    173 
    174     private boolean isChildImageUri(Uri uri) {
    175         // Sometimes, the URI of an image contains a query string with key
    176         // "bucketId" inorder to restore the image list. However, the query
    177         // string is not part of the mBaseUri. So, we check only other parts
    178         // of the two Uri to see if they are the same.
    179         Uri base = mBaseUri;
    180         return Util.equals(base.getScheme(), uri.getScheme())
    181                 && Util.equals(base.getHost(), uri.getHost())
    182                 && Util.equals(base.getAuthority(), uri.getAuthority())
    183                 && Util.equals(base.getPath(), getPathWithoutId(uri));
    184     }
    185 
    186     public IImage getImageForUri(Uri uri) {
    187         if (!isChildImageUri(uri)) return null;
    188         // Find the id of the input URI.
    189         long matchId;
    190         try {
    191             matchId = ContentUris.parseId(uri);
    192         } catch (NumberFormatException ex) {
    193             Log.i(TAG, "fail to get id in: " + uri, ex);
    194             return null;
    195         }
    196         // TODO: design a better method to get URI of specified ID
    197         Cursor cursor = getCursor();
    198         if (cursor == null) return null;
    199         synchronized (this) {
    200             cursor.moveToPosition(-1); // before first
    201             for (int i = 0; cursor.moveToNext(); ++i) {
    202                 if (getImageId(cursor) == matchId) {
    203                     BaseImage image = mCache.get(i);
    204                     if (image == null) {
    205                         image = loadImageFromCursor(cursor);
    206                         mCache.put(i, image);
    207                     }
    208                     return image;
    209                 }
    210             }
    211             return null;
    212         }
    213     }
    214 
    215     public int getImageIndex(IImage image) {
    216         return ((BaseImage) image).mIndex;
    217     }
    218 
    219     // This provides a default sorting order string for subclasses.
    220     // The list is first sorted by date, then by id. The order can be ascending
    221     // or descending, depending on the mSort variable.
    222     // The date is obtained from the "datetaken" column. But if it is null,
    223     // the "date_modified" column is used instead.
    224     protected String sortOrder() {
    225         String ascending =
    226                 (mSort == ImageManager.SORT_ASCENDING)
    227                 ? " ASC"
    228                 : " DESC";
    229 
    230         // Use DATE_TAKEN if it's non-null, otherwise use DATE_MODIFIED.
    231         // DATE_TAKEN is in milliseconds, but DATE_MODIFIED is in seconds.
    232         String dateExpr =
    233                 "case ifnull(datetaken,0)" +
    234                 " when 0 then date_modified*1000" +
    235                 " else datetaken" +
    236                 " end";
    237 
    238         // Add id to the end so that we don't ever get random sorting
    239         // which could happen, I suppose, if the date values are the same.
    240         return dateExpr + ascending + ", _id" + ascending;
    241     }
    242 }
    243