2 * Copyright (C) 2013 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.camera.data;
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;
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;
31 import java.util.ArrayList;
32 import java.util.Comparator;
33 import java.util.List;
36 * A {@link LocalDataAdapter} that provides data in the camera folder.
38 public class CameraDataAdapter implements LocalDataAdapter {
39 private static final Log.Tag TAG = new Log.Tag("CameraDataAdapter");
41 private static final int DEFAULT_DECODE_SIZE = 1600;
43 private final Context mContext;
45 private LocalDataList mImages;
47 private Listener mListener;
48 private LocalDataListener mLocalDataListener;
49 private final int mPlaceHolderResourceId;
51 private int mSuggestedWidth = DEFAULT_DECODE_SIZE;
52 private int mSuggestedHeight = DEFAULT_DECODE_SIZE;
53 private long mLastPhotoId = LocalMediaData.QUERY_ALL_MEDIA_ID;
55 private LocalData mLocalDataToDelete;
57 public CameraDataAdapter(Context context, int placeholderResource) {
59 mImages = new LocalDataList();
60 mPlaceHolderResourceId = placeholderResource;
64 public void setLocalDataListener(LocalDataListener listener) {
65 mLocalDataListener = listener;
69 public void requestLoadNewPhotos() {
70 LoadNewPhotosTask ltask = new LoadNewPhotosTask(mLastPhotoId);
71 ltask.execute(mContext.getContentResolver());
75 public void requestLoad(Callback<Void> doneCallback) {
76 QueryTask qtask = new QueryTask(doneCallback);
77 qtask.execute(mContext);
81 public AsyncTask updateMetadata(int dataId) {
82 MetadataUpdateTask result = new MetadataUpdateTask();
83 result.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, dataId);
88 public boolean isMetadataUpdated(int dataId) {
89 if (dataId < 0 || dataId >= mImages.size()) {
92 return mImages.get(dataId).isMetadataUpdated();
96 public int getItemViewType(int dataId) {
97 if (dataId < 0 || dataId >= mImages.size()) {
101 return mImages.get(dataId).getItemViewType().ordinal();
105 public LocalData getLocalData(int dataID) {
106 if (dataID < 0 || dataID >= mImages.size()) {
109 return mImages.get(dataID);
113 public int getTotalNumber() {
114 return mImages.size();
118 public ImageData getImageData(int id) {
119 return getLocalData(id);
123 public void suggestViewSizeBound(int w, int h) {
125 mSuggestedHeight = h;
129 public View getView(Context context, View recycled, int dataID,
130 ActionCallback actionCallback) {
131 if (dataID >= mImages.size() || dataID < 0) {
135 return mImages.get(dataID).getView(
136 context, recycled, mSuggestedWidth, mSuggestedHeight,
137 mPlaceHolderResourceId, this, /* inProgress */ false, actionCallback);
141 public void resizeView(Context context, int dataID, View view, int w, int h) {
142 if (dataID >= mImages.size() || dataID < 0) {
145 mImages.get(dataID).loadFullImage(context, mSuggestedWidth, mSuggestedHeight, view, this);
149 public void setListener(Listener listener) {
150 mListener = listener;
151 if (mImages.size() != 0) {
152 mListener.onDataLoaded();
157 public boolean canSwipeInFullScreen(int dataID) {
158 if (dataID < mImages.size() && dataID > 0) {
159 return mImages.get(dataID).canSwipeInFullScreen();
165 public void removeData(int dataID) {
166 LocalData d = mImages.remove(dataID);
171 // Delete previously removed data first.
173 mLocalDataToDelete = d;
174 mListener.onDataRemoved(dataID, d);
178 public boolean addData(LocalData newData) {
179 final Uri uri = newData.getUri();
180 int pos = findDataByContentUri(uri);
182 // a duplicate one, just do a substitute.
183 Log.v(TAG, "found duplicate data");
184 updateData(pos, newData);
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);
202 public boolean undoDataRemoval() {
203 if (mLocalDataToDelete == null) {
206 LocalData d = mLocalDataToDelete;
207 mLocalDataToDelete = null;
213 public boolean executeDeletion() {
214 if (mLocalDataToDelete == null) {
218 DeletionTask task = new DeletionTask();
219 task.execute(mLocalDataToDelete);
220 mLocalDataToDelete = null;
225 public void flush() {
226 replaceData(new LocalDataList());
230 public void refresh(Uri uri) {
231 final int pos = findDataByContentUri(uri);
236 LocalData data = mImages.get(pos);
237 LocalData refreshedData = data.refresh(mContext);
239 // Refresh failed. Probably removed already.
240 if (refreshedData == null && mListener != null) {
241 mListener.onDataRemoved(pos, data);
244 updateData(pos, refreshedData);
248 public void updateData(final int pos, LocalData data) {
249 mImages.set(pos, data);
250 if (mListener != null) {
251 mListener.onDataUpdated(new UpdateReporter() {
253 public boolean isDataRemoved(int dataID) {
258 public boolean isDataUpdated(int dataID) {
259 return (dataID == pos);
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
270 Comparator<LocalData> comp = new LocalData.NewestFirstComparator();
271 for (; pos < mImages.size()
272 && comp.compare(data, mImages.get(pos)) > 0; pos++) {
275 mImages.add(pos, data);
276 if (mListener != null) {
277 mListener.onDataInserted(pos, data);
281 /** Update all the data */
282 private void replaceData(LocalDataList list) {
283 if (list.size() == 0 && mImages.size() == 0) {
287 if (mListener != null) {
288 mListener.onDataLoaded();
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));
304 public void cancelItems(List<AsyncTask> loadTokens) {
305 for (AsyncTask asyncTask : loadTokens) {
306 if (asyncTask != null) {
307 asyncTask.cancel(false);
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++) {
322 public int getCount() {
323 return getTotalNumber();
326 private class LoadNewPhotosTask extends AsyncTask<ContentResolver, Void, List<LocalData>> {
328 private final long mMinPhotoId;
330 public LoadNewPhotosTask(long lastPhotoId) {
331 mMinPhotoId = lastPhotoId;
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.
340 protected List<LocalData> doInBackground(ContentResolver... contentResolvers) {
341 if (mMinPhotoId != LocalMediaData.QUERY_ALL_MEDIA_ID) {
342 final ContentResolver cr = contentResolvers[0];
343 return LocalMediaData.PhotoData.query(cr, LocalMediaData.PhotoData.CONTENT_URI,
346 return new ArrayList<LocalData>(0);
350 protected void onPostExecute(List<LocalData> newPhotoData) {
351 if (!newPhotoData.isEmpty()) {
352 LocalData newestPhoto = newPhotoData.get(0);
353 // We may overlap with another load task or a query task, in which case we want
354 // to be sure we never decrement the oldest seen id.
355 mLastPhotoId = Math.max(mLastPhotoId, newestPhoto.getContentId());
357 // We may add data that is already present, but if we do, it will be deduped in addData.
358 // addData does not dedupe session items, so we ignore them here
359 for (LocalData localData : newPhotoData) {
360 Uri sessionUri = Storage.getSessionUriFromContentUri(localData.getUri());
361 if (sessionUri == null) {
368 private class QueryTaskResult {
369 public LocalDataList mLocalDataList;
370 public long mLastPhotoId;
372 public QueryTaskResult(LocalDataList localDataList, long lastPhotoId) {
373 mLocalDataList = localDataList;
374 mLastPhotoId = lastPhotoId;
378 private class QueryTask extends AsyncTask<Context, Void, QueryTaskResult> {
379 // The maximum number of data to load metadata for in a single task.
380 private static final int MAX_METADATA = 5;
382 private final Callback<Void> mDoneCallback;
384 public QueryTask(Callback<Void> doneCallback) {
385 mDoneCallback = doneCallback;
389 * Loads all the photo and video data in the camera folder in background
390 * and combine them into one single list.
392 * @param contexts {@link Context} to load all the data.
393 * @return An {@link com.android.camera.data.CameraDataAdapter.QueryTaskResult} containing
394 * all loaded data and the highest photo id in the dataset.
397 protected QueryTaskResult doInBackground(Context... contexts) {
398 final Context context = contexts[0];
399 final ContentResolver cr = context.getContentResolver();
400 LocalDataList l = new LocalDataList();
402 List<LocalData> photoData = LocalMediaData.PhotoData.query(cr,
403 LocalMediaData.PhotoData.CONTENT_URI, LocalMediaData.QUERY_ALL_MEDIA_ID);
404 List<LocalData> videoData = LocalMediaData.VideoData.query(cr,
405 LocalMediaData.VideoData.CONTENT_URI, LocalMediaData.QUERY_ALL_MEDIA_ID);
407 long lastPhotoId = LocalMediaData.QUERY_ALL_MEDIA_ID;
408 if (!photoData.isEmpty()) {
409 lastPhotoId = photoData.get(0).getContentId();
414 l.sort(new LocalData.NewestFirstComparator());
416 // Load enough metadata so it's already loaded when we open the filmstrip.
417 for (int i = 0; i < MAX_METADATA && i < l.size(); i++) {
418 LocalData data = l.get(i);
419 MetadataLoader.loadMetadata(context, data);
421 return new QueryTaskResult(l, lastPhotoId);
425 protected void onPostExecute(QueryTaskResult result) {
426 // Since we're wiping away all of our data, we should always replace any existing last
427 // photo id with the new one we just obtained so it matches the data we're showing.
428 mLastPhotoId = result.mLastPhotoId;
429 replaceData(result.mLocalDataList);
430 if (mDoneCallback != null) {
431 mDoneCallback.onCallback(null);
433 // Now check for any photos added since this task was kicked off
434 LoadNewPhotosTask ltask = new LoadNewPhotosTask(mLastPhotoId);
435 ltask.execute(mContext.getContentResolver());
439 private class DeletionTask extends AsyncTask<LocalData, Void, Void> {
441 protected Void doInBackground(LocalData... data) {
442 for (int i = 0; i < data.length; i++) {
443 if (!data[i].isDataActionSupported(LocalData.DATA_ACTION_DELETE)) {
444 Log.v(TAG, "Deletion is not supported:" + data[i]);
447 data[i].delete(mContext);
453 private class MetadataUpdateTask extends AsyncTask<Integer, Void, List<Integer> > {
455 protected List<Integer> doInBackground(Integer... dataId) {
456 List<Integer> updatedList = new ArrayList<Integer>();
457 for (Integer id : dataId) {
458 if (id < 0 || id >= mImages.size()) {
461 final LocalData data = mImages.get(id);
462 if (MetadataLoader.loadMetadata(mContext, data)) {
470 protected void onPostExecute(final List<Integer> updatedData) {
471 // Since the metadata will affect the width and height of the data
472 // if it's a video, we need to notify the DataAdapter listener
473 // because ImageData.getWidth() and ImageData.getHeight() now may
474 // return different values due to the metadata.
475 if (mListener != null) {
476 mListener.onDataUpdated(new UpdateReporter() {
478 public boolean isDataRemoved(int dataID) {
483 public boolean isDataUpdated(int dataID) {
484 return updatedData.contains(dataID);
488 if (mLocalDataListener == null) {
491 mLocalDataListener.onMetadataUpdated(updatedData);