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.data.FilmstripItem.VideoClickedCallback; 27 import com.android.camera.debug.Log; 28 import com.android.camera.util.Callback; 29 import com.google.common.base.Optional; 30 31 import java.util.ArrayList; 32 import java.util.Comparator; 33 import java.util.Date; 34 import java.util.List; 35 36 /** 37 * A {@link LocalFilmstripDataAdapter} that provides data in the camera folder. 38 */ 39 public 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 CameraFilmstripDataAdapter(Context context, PhotoItemFactory photoItemFactory, VideoItemFactory videoItemFactory)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 setLocalDataListener(FilmstripItemListener listener)69 public void setLocalDataListener(FilmstripItemListener listener) { 70 mFilmstripItemListener = listener; 71 } 72 73 @Override requestLoadNewPhotos()74 public void requestLoadNewPhotos() { 75 LoadNewPhotosTask ltask = new LoadNewPhotosTask(mContext, mLastPhotoId); 76 ltask.execute(mContext.getContentResolver()); 77 } 78 79 @Override requestLoad(Callback<Void> onDone)80 public void requestLoad(Callback<Void> onDone) { 81 QueryTask qtask = new QueryTask(onDone); 82 qtask.execute(mContext); 83 } 84 85 @Override updateMetadataAt(int index)86 public AsyncTask updateMetadataAt(int index) { 87 return updateMetadataAt(index, false); 88 } 89 updateMetadataAt(int index, boolean forceItemUpdate)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 isMetadataUpdatedAt(int index)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 getItemViewType(int index)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 getItemAt(int index)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 getTotalNumber()122 public int getTotalNumber() { 123 return mFilmstripItems.size(); 124 } 125 126 @Override getFilmstripItemAt(int index)127 public FilmstripItem getFilmstripItemAt(int index) { 128 return getItemAt(index); 129 } 130 131 @Override suggestViewSizeBound(int w, int h)132 public void suggestViewSizeBound(int w, int h) { 133 mSuggestedWidth = w; 134 mSuggestedHeight = h; 135 } 136 137 @Override getView(View recycled, int index, VideoClickedCallback videoClickedCallback)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 setListener(Listener listener)152 public void setListener(Listener listener) { 153 mListener = listener; 154 if (mFilmstripItems.size() != 0) { 155 mListener.onFilmstripItemLoaded(); 156 } 157 } 158 159 @Override removeAt(int index)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 addOrUpdate(FilmstripItem item)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 findByContentUri(Uri uri)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 undoDeletion()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 executeDeletion()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 clear()220 public void clear() { 221 replaceItemList(new FilmstripItemList()); 222 } 223 224 @Override refresh(Uri uri)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 updateItemAt(final int pos, FilmstripItem item)243 public void updateItemAt(final int pos, FilmstripItem item) { 244 final Uri uri = item.getData().getUri(); 245 int oldPos = findByContentUri(uri); 246 mFilmstripItems.set(pos, item); 247 updateMetadataAt(pos, true /* forceItemUpdate */); 248 249 if ((oldPos != -1) && (oldPos != pos)) { 250 Log.v(TAG, "found duplicate data: " + uri); 251 removeAt(oldPos); 252 } 253 } 254 insertItem(FilmstripItem item)255 private void insertItem(FilmstripItem item) { 256 // Since this function is mostly for adding the newest data, 257 // a simple linear search should yield the best performance over a 258 // binary search. 259 int pos = 0; 260 Comparator<FilmstripItem> comp = new NewestFirstComparator( 261 new Date()); 262 for (; pos < mFilmstripItems.size() 263 && comp.compare(item, mFilmstripItems.get(pos)) > 0; pos++) { 264 } 265 mFilmstripItems.add(pos, item); 266 if (mListener != null) { 267 mListener.onFilmstripItemInserted(pos, item); 268 } 269 } 270 271 /** Update all the data */ replaceItemList(FilmstripItemList list)272 private void replaceItemList(FilmstripItemList list) { 273 if (list.size() == 0 && mFilmstripItems.size() == 0) { 274 return; 275 } 276 mFilmstripItems = list; 277 if (mListener != null) { 278 mListener.onFilmstripItemLoaded(); 279 } 280 } 281 282 @Override preloadItems(List<Integer> items)283 public List<AsyncTask> preloadItems(List<Integer> items) { 284 List<AsyncTask> result = new ArrayList<>(); 285 for (Integer id : items) { 286 if (!isMetadataUpdatedAt(id)) { 287 result.add(updateMetadataAt(id)); 288 } 289 } 290 return result; 291 } 292 293 @Override cancelItems(List<AsyncTask> loadTokens)294 public void cancelItems(List<AsyncTask> loadTokens) { 295 for (AsyncTask asyncTask : loadTokens) { 296 if (asyncTask != null) { 297 asyncTask.cancel(false); 298 } 299 } 300 } 301 302 @Override getItemsInRange(int startPosition, int endPosition)303 public List<Integer> getItemsInRange(int startPosition, int endPosition) { 304 List<Integer> result = new ArrayList<>(); 305 for (int i = Math.max(0, startPosition); i < endPosition; i++) { 306 result.add(i); 307 } 308 return result; 309 } 310 311 @Override getCount()312 public int getCount() { 313 return getTotalNumber(); 314 } 315 316 private class LoadNewPhotosTask extends AsyncTask<ContentResolver, Void, List<PhotoItem>> { 317 318 private final long mMinPhotoId; 319 private final Context mContext; 320 LoadNewPhotosTask(Context context, long lastPhotoId)321 public LoadNewPhotosTask(Context context, long lastPhotoId) { 322 mContext = context; 323 mMinPhotoId = lastPhotoId; 324 } 325 326 /** 327 * Loads any new photos added to our storage directory since our last query. 328 * @param contentResolvers {@link android.content.ContentResolver} to load data. 329 * @return An {@link java.util.ArrayList} containing any new data. 330 */ 331 @Override doInBackground(ContentResolver... contentResolvers)332 protected List<PhotoItem> doInBackground(ContentResolver... contentResolvers) { 333 if (mMinPhotoId != FilmstripItemBase.QUERY_ALL_MEDIA_ID) { 334 Log.v(TAG, "updating media metadata with photos newer than id: " + mMinPhotoId); 335 final ContentResolver cr = contentResolvers[0]; 336 return mPhotoItemFactory.queryAll(PhotoDataQuery.CONTENT_URI, mMinPhotoId); 337 } 338 return new ArrayList<>(0); 339 } 340 341 @Override onPostExecute(List<PhotoItem> newPhotoData)342 protected void onPostExecute(List<PhotoItem> newPhotoData) { 343 if (newPhotoData == null) { 344 Log.w(TAG, "null data returned from new photos query"); 345 return; 346 } 347 Log.v(TAG, "new photos query return num items: " + newPhotoData.size()); 348 if (!newPhotoData.isEmpty()) { 349 FilmstripItem newestPhoto = newPhotoData.get(0); 350 // We may overlap with another load task or a query task, in which case we want 351 // to be sure we never decrement the oldest seen id. 352 long newLastPhotoId = newestPhoto.getData().getContentId(); 353 Log.v(TAG, "updating last photo id (old:new) " + 354 mLastPhotoId + ":" + newLastPhotoId); 355 mLastPhotoId = Math.max(mLastPhotoId, newLastPhotoId); 356 } 357 // We may add data that is already present, but if we do, it will be deduped in addOrUpdate. 358 // addOrUpdate does not dedupe session items, so we ignore them here 359 for (FilmstripItem filmstripItem : newPhotoData) { 360 Uri sessionUri = Storage.instance().getSessionUriFromContentUri( 361 filmstripItem.getData().getUri()); 362 if (sessionUri == null) { 363 addOrUpdate(filmstripItem); 364 } 365 } 366 } 367 } 368 369 private class QueryTaskResult { 370 public FilmstripItemList mFilmstripItemList; 371 public long mLastPhotoId; 372 QueryTaskResult(FilmstripItemList filmstripItemList, long lastPhotoId)373 public QueryTaskResult(FilmstripItemList filmstripItemList, long lastPhotoId) { 374 mFilmstripItemList = filmstripItemList; 375 mLastPhotoId = lastPhotoId; 376 } 377 } 378 379 private class QueryTask extends AsyncTask<Context, Void, QueryTaskResult> { 380 // The maximum number of data to load metadata for in a single task. 381 private static final int MAX_METADATA = 5; 382 383 private final Callback<Void> mDoneCallback; 384 QueryTask(Callback<Void> doneCallback)385 public QueryTask(Callback<Void> doneCallback) { 386 mDoneCallback = doneCallback; 387 } 388 389 /** 390 * Loads all the photo and video data in the camera folder in background 391 * and combine them into one single list. 392 * 393 * @param contexts {@link Context} to load all the data. 394 * @return An {@link CameraFilmstripDataAdapter.QueryTaskResult} containing 395 * all loaded data and the highest photo id in the dataset. 396 */ 397 @Override doInBackground(Context... contexts)398 protected QueryTaskResult doInBackground(Context... contexts) { 399 final Context context = contexts[0]; 400 FilmstripItemList l = new FilmstripItemList(); 401 // Photos and videos 402 List<PhotoItem> photoData = mPhotoItemFactory.queryAll(); 403 List<VideoItem> videoData = mVideoItemFactory.queryAll(); 404 405 long lastPhotoId = FilmstripItemBase.QUERY_ALL_MEDIA_ID; 406 if (photoData != null && !photoData.isEmpty()) { 407 // This relies on {@link LocalMediaData.QUERY_ORDER} returning 408 // items sorted descending by ID, as such we can just pull the 409 // ID from the first item in the result to establish the last 410 // (max) photo ID. 411 FilmstripItemData firstPhotoData = photoData.get(0).getData(); 412 413 if(firstPhotoData != null) { 414 lastPhotoId = firstPhotoData.getContentId(); 415 } 416 } 417 418 if (photoData != null) { 419 Log.v(TAG, "retrieved photo metadata, number of items: " + photoData.size()); 420 l.addAll(photoData); 421 } 422 if (videoData != null) { 423 Log.v(TAG, "retrieved video metadata, number of items: " + videoData.size()); 424 l.addAll(videoData); 425 } 426 Log.v(TAG, "sorting video/photo metadata"); 427 // Photos should be sorted within photo/video by ID, which in most 428 // cases should correlate well to the date taken/modified. This sort 429 // operation makes all photos/videos sorted by date in one list. 430 l.sort(new NewestFirstComparator(new Date())); 431 Log.v(TAG, "sorted video/photo metadata"); 432 433 // Load enough metadata so it's already loaded when we open the filmstrip. 434 for (int i = 0; i < MAX_METADATA && i < l.size(); i++) { 435 FilmstripItem data = l.get(i); 436 MetadataLoader.loadMetadata(context, data); 437 } 438 return new QueryTaskResult(l, lastPhotoId); 439 } 440 441 @Override onPostExecute(QueryTaskResult result)442 protected void onPostExecute(QueryTaskResult result) { 443 // Since we're wiping away all of our data, we should always replace any existing last 444 // photo id with the new one we just obtained so it matches the data we're showing. 445 mLastPhotoId = result.mLastPhotoId; 446 replaceItemList(result.mFilmstripItemList); 447 if (mDoneCallback != null) { 448 mDoneCallback.onCallback(null); 449 } 450 // Now check for any photos added since this task was kicked off 451 LoadNewPhotosTask ltask = new LoadNewPhotosTask(mContext, mLastPhotoId); 452 ltask.execute(mContext.getContentResolver()); 453 } 454 } 455 456 private class DeletionTask extends AsyncTask<FilmstripItem, Void, Void> { 457 @Override doInBackground(FilmstripItem... items)458 protected Void doInBackground(FilmstripItem... items) { 459 for (FilmstripItem item : items) { 460 if (!item.getAttributes().canDelete()) { 461 Log.v(TAG, "Deletion is not supported:" + item); 462 continue; 463 } 464 item.delete(); 465 } 466 return null; 467 } 468 } 469 470 private class MetadataUpdateTask extends AsyncTask<Integer, Void, List<Integer> > { 471 private final boolean mForceUpdate; 472 MetadataUpdateTask(boolean forceUpdate)473 MetadataUpdateTask(boolean forceUpdate) { 474 super(); 475 mForceUpdate = forceUpdate; 476 } 477 MetadataUpdateTask()478 MetadataUpdateTask() { 479 this(false); 480 } 481 482 @Override doInBackground(Integer... dataId)483 protected List<Integer> doInBackground(Integer... dataId) { 484 List<Integer> updatedList = new ArrayList<>(); 485 for (Integer id : dataId) { 486 if (id < 0 || id >= mFilmstripItems.size()) { 487 continue; 488 } 489 final FilmstripItem data = mFilmstripItems.get(id); 490 if (MetadataLoader.loadMetadata(mContext, data) || mForceUpdate) { 491 updatedList.add(id); 492 } 493 } 494 return updatedList; 495 } 496 497 @Override onPostExecute(final List<Integer> updatedData)498 protected void onPostExecute(final List<Integer> updatedData) { 499 // Since the metadata will affect the width and height of the data 500 // if it's a video, we need to notify the DataAdapter listener 501 // because ImageData.getWidth() and ImageData.getHeight() now may 502 // return different values due to the metadata. 503 if (mListener != null) { 504 mListener.onFilmstripItemUpdated(new UpdateReporter() { 505 @Override 506 public boolean isDataRemoved(int index) { 507 return false; 508 } 509 510 @Override 511 public boolean isDataUpdated(int index) { 512 return updatedData.contains(index); 513 } 514 }); 515 } 516 if (mFilmstripItemListener == null) { 517 return; 518 } 519 mFilmstripItemListener.onMetadataUpdated(updatedData); 520 } 521 } 522 } 523