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