1 /* 2 * Copyright (C) 2013 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.data; 18 19 import android.content.ContentResolver; 20 import android.content.Context; 21 import android.net.Uri; 22 import android.os.AsyncTask; 23 import android.view.View; 24 25 import com.android.camera.Storage; 26 import com.android.camera.debug.Log; 27 import com.android.camera.filmstrip.ImageData; 28 import com.android.camera.util.Callback; 29 30 import java.util.ArrayList; 31 import java.util.Comparator; 32 import java.util.List; 33 34 /** 35 * A {@link LocalDataAdapter} that provides data in the camera folder. 36 */ 37 public class CameraDataAdapter implements LocalDataAdapter { 38 private static final Log.Tag TAG = new Log.Tag("CameraDataAdapter"); 39 40 private static final int DEFAULT_DECODE_SIZE = 1600; 41 42 private final Context mContext; 43 44 private LocalDataList mImages; 45 46 private Listener mListener; 47 private LocalDataListener mLocalDataListener; 48 private final int mPlaceHolderResourceId; 49 50 private int mSuggestedWidth = DEFAULT_DECODE_SIZE; 51 private int mSuggestedHeight = DEFAULT_DECODE_SIZE; 52 private long mLastPhotoId = LocalMediaData.QUERY_ALL_MEDIA_ID; 53 54 private LocalData mLocalDataToDelete; 55 56 public CameraDataAdapter(Context context, int placeholderResource) { 57 mContext = context; 58 mImages = new LocalDataList(); 59 mPlaceHolderResourceId = placeholderResource; 60 } 61 62 @Override 63 public void setLocalDataListener(LocalDataListener listener) { 64 mLocalDataListener = listener; 65 } 66 67 @Override 68 public void requestLoadNewPhotos() { 69 LoadNewPhotosTask ltask = new LoadNewPhotosTask(mLastPhotoId); 70 ltask.execute(mContext.getContentResolver()); 71 } 72 73 @Override 74 public void requestLoad(Callback<Void> doneCallback) { 75 QueryTask qtask = new QueryTask(doneCallback); 76 qtask.execute(mContext); 77 } 78 79 @Override 80 public AsyncTask updateMetadata(int dataId) { 81 MetadataUpdateTask result = new MetadataUpdateTask(); 82 result.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, dataId); 83 return result; 84 } 85 86 @Override 87 public boolean isMetadataUpdated(int dataId) { 88 if (dataId < 0 || dataId >= mImages.size()) { 89 return true; 90 } 91 return mImages.get(dataId).isMetadataUpdated(); 92 } 93 94 @Override 95 public int getItemViewType(int dataId) { 96 if (dataId < 0 || dataId >= mImages.size()) { 97 return -1; 98 } 99 100 return mImages.get(dataId).getItemViewType().ordinal(); 101 } 102 103 @Override 104 public LocalData getLocalData(int dataID) { 105 if (dataID < 0 || dataID >= mImages.size()) { 106 return null; 107 } 108 return mImages.get(dataID); 109 } 110 111 @Override 112 public int getTotalNumber() { 113 return mImages.size(); 114 } 115 116 @Override 117 public ImageData getImageData(int id) { 118 return getLocalData(id); 119 } 120 121 @Override 122 public void suggestViewSizeBound(int w, int h) { 123 mSuggestedWidth = w; 124 mSuggestedHeight = h; 125 } 126 127 @Override 128 public View getView(Context context, View recycled, int dataID) { 129 if (dataID >= mImages.size() || dataID < 0) { 130 return null; 131 } 132 133 return mImages.get(dataID).getView( 134 context, recycled, mSuggestedWidth, mSuggestedHeight, 135 mPlaceHolderResourceId, this, /* inProgress */ false); 136 } 137 138 @Override 139 public void resizeView(Context context, int dataID, View view, int w, int h) { 140 if (dataID >= mImages.size() || dataID < 0) { 141 return; 142 } 143 mImages.get(dataID).loadFullImage(context, mSuggestedWidth, mSuggestedHeight, view, this); 144 } 145 146 @Override 147 public void setListener(Listener listener) { 148 mListener = listener; 149 if (mImages.size() != 0) { 150 mListener.onDataLoaded(); 151 } 152 } 153 154 @Override 155 public boolean canSwipeInFullScreen(int dataID) { 156 if (dataID < mImages.size() && dataID > 0) { 157 return mImages.get(dataID).canSwipeInFullScreen(); 158 } 159 return true; 160 } 161 162 @Override 163 public void removeData(int dataID) { 164 LocalData d = mImages.remove(dataID); 165 if (d == null) { 166 return; 167 } 168 169 // Delete previously removed data first. 170 executeDeletion(); 171 mLocalDataToDelete = d; 172 mListener.onDataRemoved(dataID, d); 173 } 174 175 @Override 176 public boolean addData(LocalData newData) { 177 final Uri uri = newData.getUri(); 178 int pos = findDataByContentUri(uri); 179 if (pos != -1) { 180 // a duplicate one, just do a substitute. 181 Log.v(TAG, "found duplicate data"); 182 updateData(pos, newData); 183 return false; 184 } else { 185 // a new data. 186 insertData(newData); 187 return true; 188 } 189 } 190 191 @Override 192 public int findDataByContentUri(Uri uri) { 193 // LocalDataList will return in O(1) if the uri is not contained. 194 // Otherwise the performance is O(n), but this is acceptable as we will 195 // most often call this to find an element at the beginning of the list. 196 return mImages.indexOf(uri); 197 } 198 199 @Override 200 public boolean undoDataRemoval() { 201 if (mLocalDataToDelete == null) { 202 return false; 203 } 204 LocalData d = mLocalDataToDelete; 205 mLocalDataToDelete = null; 206 insertData(d); 207 return true; 208 } 209 210 @Override 211 public boolean executeDeletion() { 212 if (mLocalDataToDelete == null) { 213 return false; 214 } 215 216 DeletionTask task = new DeletionTask(); 217 task.execute(mLocalDataToDelete); 218 mLocalDataToDelete = null; 219 return true; 220 } 221 222 @Override 223 public void flush() { 224 replaceData(new LocalDataList()); 225 } 226 227 @Override 228 public void refresh(Uri uri) { 229 final int pos = findDataByContentUri(uri); 230 if (pos == -1) { 231 return; 232 } 233 234 LocalData data = mImages.get(pos); 235 LocalData refreshedData = data.refresh(mContext); 236 237 // Refresh failed. Probably removed already. 238 if (refreshedData == null && mListener != null) { 239 mListener.onDataRemoved(pos, data); 240 return; 241 } 242 updateData(pos, refreshedData); 243 } 244 245 @Override 246 public void updateData(final int pos, LocalData data) { 247 mImages.set(pos, data); 248 if (mListener != null) { 249 mListener.onDataUpdated(new UpdateReporter() { 250 @Override 251 public boolean isDataRemoved(int dataID) { 252 return false; 253 } 254 255 @Override 256 public boolean isDataUpdated(int dataID) { 257 return (dataID == pos); 258 } 259 }); 260 } 261 } 262 263 private void insertData(LocalData data) { 264 // Since this function is mostly for adding the newest data, 265 // a simple linear search should yield the best performance over a 266 // binary search. 267 int pos = 0; 268 Comparator<LocalData> comp = new LocalData.NewestFirstComparator(); 269 for (; pos < mImages.size() 270 && comp.compare(data, mImages.get(pos)) > 0; pos++) { 271 ; 272 } 273 mImages.add(pos, data); 274 if (mListener != null) { 275 mListener.onDataInserted(pos, data); 276 } 277 } 278 279 /** Update all the data */ 280 private void replaceData(LocalDataList list) { 281 if (list.size() == 0 && mImages.size() == 0) { 282 return; 283 } 284 mImages = list; 285 if (mListener != null) { 286 mListener.onDataLoaded(); 287 } 288 } 289 290 @Override 291 public List<AsyncTask> preloadItems(List<Integer> items) { 292 List<AsyncTask> result = new ArrayList<AsyncTask>(); 293 for (Integer id : items) { 294 if (!isMetadataUpdated(id)) { 295 result.add(updateMetadata(id)); 296 } 297 } 298 return result; 299 } 300 301 @Override 302 public void cancelItems(List<AsyncTask> loadTokens) { 303 for (AsyncTask asyncTask : loadTokens) { 304 if (asyncTask != null) { 305 asyncTask.cancel(false); 306 } 307 } 308 } 309 310 @Override 311 public List<Integer> getItemsInRange(int startPosition, int endPosition) { 312 List<Integer> result = new ArrayList<Integer>(); 313 for (int i = Math.max(0, startPosition); i < endPosition; i++) { 314 result.add(i); 315 } 316 return result; 317 } 318 319 @Override 320 public int getCount() { 321 return getTotalNumber(); 322 } 323 324 private class LoadNewPhotosTask extends AsyncTask<ContentResolver, Void, List<LocalData>> { 325 326 private final long mMinPhotoId; 327 328 public LoadNewPhotosTask(long lastPhotoId) { 329 mMinPhotoId = lastPhotoId; 330 } 331 332 /** 333 * Loads any new photos added to our storage directory since our last query. 334 * @param contentResolvers {@link android.content.ContentResolver} to load data. 335 * @return An {@link java.util.ArrayList} containing any new data. 336 */ 337 @Override 338 protected List<LocalData> doInBackground(ContentResolver... contentResolvers) { 339 if (mMinPhotoId != LocalMediaData.QUERY_ALL_MEDIA_ID) { 340 final ContentResolver cr = contentResolvers[0]; 341 return LocalMediaData.PhotoData.query(cr, LocalMediaData.PhotoData.CONTENT_URI, 342 mMinPhotoId); 343 } 344 return new ArrayList<LocalData>(0); 345 } 346 347 @Override 348 protected void onPostExecute(List<LocalData> newPhotoData) { 349 if (!newPhotoData.isEmpty()) { 350 LocalData newestPhoto = newPhotoData.get(0); 351 // We may overlap with another load task or a query task, in which case we want 352 // to be sure we never decrement the oldest seen id. 353 mLastPhotoId = Math.max(mLastPhotoId, newestPhoto.getContentId()); 354 } 355 // We may add data that is already present, but if we do, it will be deduped in addData. 356 // addData does not dedupe session items, so we ignore them here 357 for (LocalData localData : newPhotoData) { 358 Uri sessionUri = Storage.getSessionUriFromContentUri(localData.getUri()); 359 if (sessionUri == null) { 360 addData(localData); 361 } 362 } 363 } 364 } 365 366 private class QueryTaskResult { 367 public LocalDataList mLocalDataList; 368 public long mLastPhotoId; 369 370 public QueryTaskResult(LocalDataList localDataList, long lastPhotoId) { 371 mLocalDataList = localDataList; 372 mLastPhotoId = lastPhotoId; 373 } 374 } 375 376 private class QueryTask extends AsyncTask<Context, Void, QueryTaskResult> { 377 // The maximum number of data to load metadata for in a single task. 378 private static final int MAX_METADATA = 5; 379 380 private final Callback<Void> mDoneCallback; 381 382 public QueryTask(Callback<Void> doneCallback) { 383 mDoneCallback = doneCallback; 384 } 385 386 /** 387 * Loads all the photo and video data in the camera folder in background 388 * and combine them into one single list. 389 * 390 * @param contexts {@link Context} to load all the data. 391 * @return An {@link com.android.camera.data.CameraDataAdapter.QueryTaskResult} containing 392 * all loaded data and the highest photo id in the dataset. 393 */ 394 @Override 395 protected QueryTaskResult doInBackground(Context... contexts) { 396 final Context context = contexts[0]; 397 final ContentResolver cr = context.getContentResolver(); 398 LocalDataList l = new LocalDataList(); 399 // Photos 400 List<LocalData> photoData = LocalMediaData.PhotoData.query(cr, 401 LocalMediaData.PhotoData.CONTENT_URI, LocalMediaData.QUERY_ALL_MEDIA_ID); 402 List<LocalData> videoData = LocalMediaData.VideoData.query(cr, 403 LocalMediaData.VideoData.CONTENT_URI, LocalMediaData.QUERY_ALL_MEDIA_ID); 404 405 long lastPhotoId = LocalMediaData.QUERY_ALL_MEDIA_ID; 406 if (!photoData.isEmpty()) { 407 lastPhotoId = photoData.get(0).getContentId(); 408 } 409 410 l.addAll(photoData); 411 l.addAll(videoData); 412 l.sort(new LocalData.NewestFirstComparator()); 413 414 // Load enough metadata so it's already loaded when we open the filmstrip. 415 for (int i = 0; i < MAX_METADATA && i < l.size(); i++) { 416 LocalData data = l.get(i); 417 MetadataLoader.loadMetadata(context, data); 418 } 419 return new QueryTaskResult(l, lastPhotoId); 420 } 421 422 @Override 423 protected void onPostExecute(QueryTaskResult result) { 424 // Since we're wiping away all of our data, we should always replace any existing last 425 // photo id with the new one we just obtained so it matches the data we're showing. 426 mLastPhotoId = result.mLastPhotoId; 427 replaceData(result.mLocalDataList); 428 if (mDoneCallback != null) { 429 mDoneCallback.onCallback(null); 430 } 431 // Now check for any photos added since this task was kicked off 432 LoadNewPhotosTask ltask = new LoadNewPhotosTask(mLastPhotoId); 433 ltask.execute(mContext.getContentResolver()); 434 } 435 } 436 437 private class DeletionTask extends AsyncTask<LocalData, Void, Void> { 438 @Override 439 protected Void doInBackground(LocalData... data) { 440 for (int i = 0; i < data.length; i++) { 441 if (!data[i].isDataActionSupported(LocalData.DATA_ACTION_DELETE)) { 442 Log.v(TAG, "Deletion is not supported:" + data[i]); 443 continue; 444 } 445 data[i].delete(mContext); 446 } 447 return null; 448 } 449 } 450 451 private class MetadataUpdateTask extends AsyncTask<Integer, Void, List<Integer> > { 452 @Override 453 protected List<Integer> doInBackground(Integer... dataId) { 454 List<Integer> updatedList = new ArrayList<Integer>(); 455 for (Integer id : dataId) { 456 if (id < 0 || id >= mImages.size()) { 457 continue; 458 } 459 final LocalData data = mImages.get(id); 460 if (MetadataLoader.loadMetadata(mContext, data)) { 461 updatedList.add(id); 462 } 463 } 464 return updatedList; 465 } 466 467 @Override 468 protected void onPostExecute(final List<Integer> updatedData) { 469 // Since the metadata will affect the width and height of the data 470 // if it's a video, we need to notify the DataAdapter listener 471 // because ImageData.getWidth() and ImageData.getHeight() now may 472 // return different values due to the metadata. 473 if (mListener != null) { 474 mListener.onDataUpdated(new UpdateReporter() { 475 @Override 476 public boolean isDataRemoved(int dataID) { 477 return false; 478 } 479 480 @Override 481 public boolean isDataUpdated(int dataID) { 482 return updatedData.contains(dataID); 483 } 484 }); 485 } 486 if (mLocalDataListener == null) { 487 return; 488 } 489 mLocalDataListener.onMetadataUpdated(updatedData); 490 } 491 } 492 } 493