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