• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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