blob: 721b213bff0ac7f79ee649da13e2e30c3d50af55 [file] [log] [blame]
/*
* 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<Void> 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<LocalData> 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<AsyncTask> preloadItems(List<Integer> items) {
List<AsyncTask> result = new ArrayList<AsyncTask>();
for (Integer id : items) {
if (!isMetadataUpdated(id)) {
result.add(updateMetadata(id));
}
}
return result;
}
@Override
public void cancelItems(List<AsyncTask> loadTokens) {
for (AsyncTask asyncTask : loadTokens) {
if (asyncTask != null) {
asyncTask.cancel(false);
}
}
}
@Override
public List<Integer> getItemsInRange(int startPosition, int endPosition) {
List<Integer> result = new ArrayList<Integer>();
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<ContentResolver, Void, List<LocalData>> {
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<LocalData> 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<LocalData>(0);
}
@Override
protected void onPostExecute(List<LocalData> 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<Context, Void, QueryTaskResult> {
// The maximum number of data to load metadata for in a single task.
private static final int MAX_METADATA = 5;
private final Callback<Void> mDoneCallback;
public QueryTask(Callback<Void> 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<LocalData> photoData = LocalMediaData.PhotoData.query(cr,
LocalMediaData.PhotoData.CONTENT_URI, LocalMediaData.QUERY_ALL_MEDIA_ID);
List<LocalData> 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<LocalData, Void, Void> {
@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<Integer, Void, List<Integer> > {
@Override
protected List<Integer> doInBackground(Integer... dataId) {
List<Integer> updatedList = new ArrayList<Integer>();
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<Integer> 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);
}
}
}