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