• 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         mFilmstripItems.set(pos, item);
245         updateMetadataAt(pos, true /* forceItemUpdate */);
246     }
247 
insertItem(FilmstripItem item)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 */
replaceItemList(FilmstripItemList list)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
preloadItems(List<Integer> items)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
cancelItems(List<AsyncTask> loadTokens)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
getItemsInRange(int startPosition, int endPosition)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
getCount()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 
LoadNewPhotosTask(Context context, long lastPhotoId)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
doInBackground(ContentResolver... contentResolvers)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
onPostExecute(List<PhotoItem> newPhotoData)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 
QueryTaskResult(FilmstripItemList filmstripItemList, long lastPhotoId)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 
QueryTask(Callback<Void> doneCallback)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
doInBackground(Context... contexts)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
onPostExecute(QueryTaskResult result)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
doInBackground(FilmstripItem... items)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 
MetadataUpdateTask(boolean forceUpdate)466         MetadataUpdateTask(boolean forceUpdate) {
467             super();
468             mForceUpdate = forceUpdate;
469         }
470 
MetadataUpdateTask()471         MetadataUpdateTask() {
472             this(false);
473         }
474 
475         @Override
doInBackground(Integer... dataId)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
onPostExecute(final List<Integer> updatedData)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