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 
BaseImageList(ContentResolver resolver, Uri uri, int sort, String bucketId)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 
close()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
contentUri(long id)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 
getCount()95     public int getCount() {
96         Cursor cursor = getCursor();
97         if (cursor == null) return 0;
98         synchronized (this) {
99             return cursor.getCount();
100         }
101     }
102 
isEmpty()103     public boolean isEmpty() {
104         return getCount() == 0;
105     }
106 
getCursor()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 
getImageAt(int i)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 
removeImage(IImage image)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 
removeImageAt(int i)145     public boolean removeImageAt(int i) {
146         // TODO: need to delete the thumbnails as well
147         return removeImage(getImageAt(i));
148     }
149 
createCursor()150     protected abstract Cursor createCursor();
151 
loadImageFromCursor(Cursor cursor)152     protected abstract BaseImage loadImageFromCursor(Cursor cursor);
153 
getImageId(Cursor cursor)154     protected abstract long getImageId(Cursor cursor);
155 
invalidateCursor()156     protected void invalidateCursor() {
157         if (mCursor == null) return;
158         mCursor.deactivate();
159         mCursorDeactivated = true;
160     }
161 
invalidateCache()162     protected void invalidateCache() {
163         mCache.clear();
164     }
165 
166     private static final Pattern sPathWithId = Pattern.compile("(.*)/\\d+");
167 
getPathWithoutId(Uri uri)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 
isChildImageUri(Uri uri)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 
getImageForUri(Uri uri)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 
getImageIndex(IImage image)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.
sortOrder()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