/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.camera.data; import android.content.ContentResolver; import android.content.Context; import android.net.Uri; import android.os.AsyncTask; import android.view.View; import com.android.camera.Storage; import com.android.camera.debug.Log; import com.android.camera.filmstrip.ImageData; import com.android.camera.util.Callback; import java.util.ArrayList; import java.util.Comparator; import java.util.List; /** * A {@link LocalDataAdapter} that provides data in the camera folder. */ public class CameraDataAdapter implements LocalDataAdapter { private static final Log.Tag TAG = new Log.Tag("CameraDataAdapter"); private static final int DEFAULT_DECODE_SIZE = 1600; private final Context mContext; private LocalDataList mImages; private Listener mListener; private LocalDataListener mLocalDataListener; private final int mPlaceHolderResourceId; private int mSuggestedWidth = DEFAULT_DECODE_SIZE; private int mSuggestedHeight = DEFAULT_DECODE_SIZE; private long mLastPhotoId = LocalMediaData.QUERY_ALL_MEDIA_ID; private LocalData mLocalDataToDelete; public CameraDataAdapter(Context context, int placeholderResource) { mContext = context; mImages = new LocalDataList(); mPlaceHolderResourceId = placeholderResource; } @Override public void setLocalDataListener(LocalDataListener listener) { mLocalDataListener = listener; } @Override public void requestLoadNewPhotos() { LoadNewPhotosTask ltask = new LoadNewPhotosTask(mLastPhotoId); ltask.execute(mContext.getContentResolver()); } @Override public void requestLoad(Callback doneCallback) { QueryTask qtask = new QueryTask(doneCallback); qtask.execute(mContext); } @Override public AsyncTask updateMetadata(int dataId) { MetadataUpdateTask result = new MetadataUpdateTask(); result.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, dataId); return result; } @Override public boolean isMetadataUpdated(int dataId) { if (dataId < 0 || dataId >= mImages.size()) { return true; } return mImages.get(dataId).isMetadataUpdated(); } @Override public int getItemViewType(int dataId) { if (dataId < 0 || dataId >= mImages.size()) { return -1; } return mImages.get(dataId).getItemViewType().ordinal(); } @Override public LocalData getLocalData(int dataID) { if (dataID < 0 || dataID >= mImages.size()) { return null; } return mImages.get(dataID); } @Override public int getTotalNumber() { return mImages.size(); } @Override public ImageData getImageData(int id) { return getLocalData(id); } @Override public void suggestViewSizeBound(int w, int h) { mSuggestedWidth = w; mSuggestedHeight = h; } @Override public View getView(Context context, View recycled, int dataID) { if (dataID >= mImages.size() || dataID < 0) { return null; } return mImages.get(dataID).getView( context, recycled, mSuggestedWidth, mSuggestedHeight, mPlaceHolderResourceId, this, /* inProgress */ false); } @Override public void resizeView(Context context, int dataID, View view, int w, int h) { if (dataID >= mImages.size() || dataID < 0) { return; } mImages.get(dataID).loadFullImage(context, mSuggestedWidth, mSuggestedHeight, view, this); } @Override public void setListener(Listener listener) { mListener = listener; if (mImages.size() != 0) { mListener.onDataLoaded(); } } @Override public boolean canSwipeInFullScreen(int dataID) { if (dataID < mImages.size() && dataID > 0) { return mImages.get(dataID).canSwipeInFullScreen(); } return true; } @Override public void removeData(int dataID) { LocalData d = mImages.remove(dataID); if (d == null) { return; } // Delete previously removed data first. executeDeletion(); mLocalDataToDelete = d; mListener.onDataRemoved(dataID, d); } @Override public boolean addData(LocalData newData) { final Uri uri = newData.getUri(); int pos = findDataByContentUri(uri); if (pos != -1) { // a duplicate one, just do a substitute. Log.v(TAG, "found duplicate data"); updateData(pos, newData); return false; } else { // a new data. insertData(newData); return true; } } @Override public int findDataByContentUri(Uri uri) { // LocalDataList will return in O(1) if the uri is not contained. // Otherwise the performance is O(n), but this is acceptable as we will // most often call this to find an element at the beginning of the list. return mImages.indexOf(uri); } @Override public boolean undoDataRemoval() { if (mLocalDataToDelete == null) { return false; } LocalData d = mLocalDataToDelete; mLocalDataToDelete = null; insertData(d); return true; } @Override public boolean executeDeletion() { if (mLocalDataToDelete == null) { return false; } DeletionTask task = new DeletionTask(); task.execute(mLocalDataToDelete); mLocalDataToDelete = null; return true; } @Override public void flush() { replaceData(new LocalDataList()); } @Override public void refresh(Uri uri) { final int pos = findDataByContentUri(uri); if (pos == -1) { return; } LocalData data = mImages.get(pos); LocalData refreshedData = data.refresh(mContext); // Refresh failed. Probably removed already. if (refreshedData == null && mListener != null) { mListener.onDataRemoved(pos, data); return; } updateData(pos, refreshedData); } @Override public void updateData(final int pos, LocalData data) { mImages.set(pos, data); if (mListener != null) { mListener.onDataUpdated(new UpdateReporter() { @Override public boolean isDataRemoved(int dataID) { return false; } @Override public boolean isDataUpdated(int dataID) { return (dataID == pos); } }); } } private void insertData(LocalData data) { // Since this function is mostly for adding the newest data, // a simple linear search should yield the best performance over a // binary search. int pos = 0; Comparator comp = new LocalData.NewestFirstComparator(); for (; pos < mImages.size() && comp.compare(data, mImages.get(pos)) > 0; pos++) { ; } mImages.add(pos, data); if (mListener != null) { mListener.onDataInserted(pos, data); } } /** Update all the data */ private void replaceData(LocalDataList list) { if (list.size() == 0 && mImages.size() == 0) { return; } mImages = list; if (mListener != null) { mListener.onDataLoaded(); } } @Override public List preloadItems(List items) { List result = new ArrayList(); for (Integer id : items) { if (!isMetadataUpdated(id)) { result.add(updateMetadata(id)); } } return result; } @Override public void cancelItems(List loadTokens) { for (AsyncTask asyncTask : loadTokens) { if (asyncTask != null) { asyncTask.cancel(false); } } } @Override public List getItemsInRange(int startPosition, int endPosition) { List result = new ArrayList(); for (int i = Math.max(0, startPosition); i < endPosition; i++) { result.add(i); } return result; } @Override public int getCount() { return getTotalNumber(); } private class LoadNewPhotosTask extends AsyncTask> { private final long mMinPhotoId; public LoadNewPhotosTask(long lastPhotoId) { mMinPhotoId = lastPhotoId; } /** * Loads any new photos added to our storage directory since our last query. * @param contentResolvers {@link android.content.ContentResolver} to load data. * @return An {@link java.util.ArrayList} containing any new data. */ @Override protected List doInBackground(ContentResolver... contentResolvers) { if (mMinPhotoId != LocalMediaData.QUERY_ALL_MEDIA_ID) { final ContentResolver cr = contentResolvers[0]; return LocalMediaData.PhotoData.query(cr, LocalMediaData.PhotoData.CONTENT_URI, mMinPhotoId); } return new ArrayList(0); } @Override protected void onPostExecute(List newPhotoData) { if (!newPhotoData.isEmpty()) { LocalData newestPhoto = newPhotoData.get(0); // We may overlap with another load task or a query task, in which case we want // to be sure we never decrement the oldest seen id. mLastPhotoId = Math.max(mLastPhotoId, newestPhoto.getContentId()); } // We may add data that is already present, but if we do, it will be deduped in addData. // addData does not dedupe session items, so we ignore them here for (LocalData localData : newPhotoData) { Uri sessionUri = Storage.getSessionUriFromContentUri(localData.getUri()); if (sessionUri == null) { addData(localData); } } } } private class QueryTaskResult { public LocalDataList mLocalDataList; public long mLastPhotoId; public QueryTaskResult(LocalDataList localDataList, long lastPhotoId) { mLocalDataList = localDataList; mLastPhotoId = lastPhotoId; } } private class QueryTask extends AsyncTask { // The maximum number of data to load metadata for in a single task. private static final int MAX_METADATA = 5; private final Callback mDoneCallback; public QueryTask(Callback doneCallback) { mDoneCallback = doneCallback; } /** * Loads all the photo and video data in the camera folder in background * and combine them into one single list. * * @param contexts {@link Context} to load all the data. * @return An {@link com.android.camera.data.CameraDataAdapter.QueryTaskResult} containing * all loaded data and the highest photo id in the dataset. */ @Override protected QueryTaskResult doInBackground(Context... contexts) { final Context context = contexts[0]; final ContentResolver cr = context.getContentResolver(); LocalDataList l = new LocalDataList(); // Photos List photoData = LocalMediaData.PhotoData.query(cr, LocalMediaData.PhotoData.CONTENT_URI, LocalMediaData.QUERY_ALL_MEDIA_ID); List videoData = LocalMediaData.VideoData.query(cr, LocalMediaData.VideoData.CONTENT_URI, LocalMediaData.QUERY_ALL_MEDIA_ID); long lastPhotoId = LocalMediaData.QUERY_ALL_MEDIA_ID; if (!photoData.isEmpty()) { lastPhotoId = photoData.get(0).getContentId(); } l.addAll(photoData); l.addAll(videoData); l.sort(new LocalData.NewestFirstComparator()); // Load enough metadata so it's already loaded when we open the filmstrip. for (int i = 0; i < MAX_METADATA && i < l.size(); i++) { LocalData data = l.get(i); MetadataLoader.loadMetadata(context, data); } return new QueryTaskResult(l, lastPhotoId); } @Override protected void onPostExecute(QueryTaskResult result) { // Since we're wiping away all of our data, we should always replace any existing last // photo id with the new one we just obtained so it matches the data we're showing. mLastPhotoId = result.mLastPhotoId; replaceData(result.mLocalDataList); if (mDoneCallback != null) { mDoneCallback.onCallback(null); } // Now check for any photos added since this task was kicked off LoadNewPhotosTask ltask = new LoadNewPhotosTask(mLastPhotoId); ltask.execute(mContext.getContentResolver()); } } private class DeletionTask extends AsyncTask { @Override protected Void doInBackground(LocalData... data) { for (int i = 0; i < data.length; i++) { if (!data[i].isDataActionSupported(LocalData.DATA_ACTION_DELETE)) { Log.v(TAG, "Deletion is not supported:" + data[i]); continue; } data[i].delete(mContext); } return null; } } private class MetadataUpdateTask extends AsyncTask > { @Override protected List doInBackground(Integer... dataId) { List updatedList = new ArrayList(); for (Integer id : dataId) { if (id < 0 || id >= mImages.size()) { continue; } final LocalData data = mImages.get(id); if (MetadataLoader.loadMetadata(mContext, data)) { updatedList.add(id); } } return updatedList; } @Override protected void onPostExecute(final List updatedData) { // Since the metadata will affect the width and height of the data // if it's a video, we need to notify the DataAdapter listener // because ImageData.getWidth() and ImageData.getHeight() now may // return different values due to the metadata. if (mListener != null) { mListener.onDataUpdated(new UpdateReporter() { @Override public boolean isDataRemoved(int dataID) { return false; } @Override public boolean isDataUpdated(int dataID) { return updatedData.contains(dataID); } }); } if (mLocalDataListener == null) { return; } mLocalDataListener.onMetadataUpdated(updatedData); } } }