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 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