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