Home | History | Annotate | Download | only in data
      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.debug.Log;
     27 import com.android.camera.filmstrip.ImageData;
     28 import com.android.camera.util.Callback;
     29 
     30 import java.util.ArrayList;
     31 import java.util.Comparator;
     32 import java.util.List;
     33 
     34 /**
     35  * A {@link LocalDataAdapter} that provides data in the camera folder.
     36  */
     37 public class CameraDataAdapter implements LocalDataAdapter {
     38     private static final Log.Tag TAG = new Log.Tag("CameraDataAdapter");
     39 
     40     private static final int DEFAULT_DECODE_SIZE = 1600;
     41 
     42     private final Context mContext;
     43 
     44     private LocalDataList mImages;
     45 
     46     private Listener mListener;
     47     private LocalDataListener mLocalDataListener;
     48     private final int mPlaceHolderResourceId;
     49 
     50     private int mSuggestedWidth = DEFAULT_DECODE_SIZE;
     51     private int mSuggestedHeight = DEFAULT_DECODE_SIZE;
     52     private long mLastPhotoId = LocalMediaData.QUERY_ALL_MEDIA_ID;
     53 
     54     private LocalData mLocalDataToDelete;
     55 
     56     public CameraDataAdapter(Context context, int placeholderResource) {
     57         mContext = context;
     58         mImages = new LocalDataList();
     59         mPlaceHolderResourceId = placeholderResource;
     60     }
     61 
     62     @Override
     63     public void setLocalDataListener(LocalDataListener listener) {
     64         mLocalDataListener = listener;
     65     }
     66 
     67     @Override
     68     public void requestLoadNewPhotos() {
     69         LoadNewPhotosTask ltask = new LoadNewPhotosTask(mLastPhotoId);
     70         ltask.execute(mContext.getContentResolver());
     71     }
     72 
     73     @Override
     74     public void requestLoad(Callback<Void> doneCallback) {
     75         QueryTask qtask = new QueryTask(doneCallback);
     76         qtask.execute(mContext);
     77     }
     78 
     79     @Override
     80     public AsyncTask updateMetadata(int dataId) {
     81         MetadataUpdateTask result = new MetadataUpdateTask();
     82         result.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, dataId);
     83         return result;
     84     }
     85 
     86     @Override
     87     public boolean isMetadataUpdated(int dataId) {
     88         if (dataId < 0 || dataId >= mImages.size()) {
     89             return true;
     90         }
     91         return mImages.get(dataId).isMetadataUpdated();
     92     }
     93 
     94     @Override
     95     public int getItemViewType(int dataId) {
     96         if (dataId < 0 || dataId >= mImages.size()) {
     97             return -1;
     98         }
     99 
    100         return mImages.get(dataId).getItemViewType().ordinal();
    101     }
    102 
    103     @Override
    104     public LocalData getLocalData(int dataID) {
    105         if (dataID < 0 || dataID >= mImages.size()) {
    106             return null;
    107         }
    108         return mImages.get(dataID);
    109     }
    110 
    111     @Override
    112     public int getTotalNumber() {
    113         return mImages.size();
    114     }
    115 
    116     @Override
    117     public ImageData getImageData(int id) {
    118         return getLocalData(id);
    119     }
    120 
    121     @Override
    122     public void suggestViewSizeBound(int w, int h) {
    123         mSuggestedWidth = w;
    124         mSuggestedHeight = h;
    125     }
    126 
    127     @Override
    128     public View getView(Context context, View recycled, int dataID) {
    129         if (dataID >= mImages.size() || dataID < 0) {
    130             return null;
    131         }
    132 
    133         return mImages.get(dataID).getView(
    134                 context, recycled, mSuggestedWidth, mSuggestedHeight,
    135                 mPlaceHolderResourceId, this, /* inProgress */ false);
    136     }
    137 
    138     @Override
    139     public void resizeView(Context context, int dataID, View view, int w, int h) {
    140         if (dataID >= mImages.size() || dataID < 0) {
    141             return;
    142         }
    143         mImages.get(dataID).loadFullImage(context, mSuggestedWidth, mSuggestedHeight, view, this);
    144     }
    145 
    146     @Override
    147     public void setListener(Listener listener) {
    148         mListener = listener;
    149         if (mImages.size() != 0) {
    150             mListener.onDataLoaded();
    151         }
    152     }
    153 
    154     @Override
    155     public boolean canSwipeInFullScreen(int dataID) {
    156         if (dataID < mImages.size() && dataID > 0) {
    157             return mImages.get(dataID).canSwipeInFullScreen();
    158         }
    159         return true;
    160     }
    161 
    162     @Override
    163     public void removeData(int dataID) {
    164         LocalData d = mImages.remove(dataID);
    165         if (d == null) {
    166             return;
    167         }
    168 
    169         // Delete previously removed data first.
    170         executeDeletion();
    171         mLocalDataToDelete = d;
    172         mListener.onDataRemoved(dataID, d);
    173     }
    174 
    175     @Override
    176     public boolean addData(LocalData newData) {
    177         final Uri uri = newData.getUri();
    178         int pos = findDataByContentUri(uri);
    179         if (pos != -1) {
    180             // a duplicate one, just do a substitute.
    181             Log.v(TAG, "found duplicate data");
    182             updateData(pos, newData);
    183             return false;
    184         } else {
    185             // a new data.
    186             insertData(newData);
    187             return true;
    188         }
    189     }
    190 
    191     @Override
    192     public int findDataByContentUri(Uri uri) {
    193         // LocalDataList will return in O(1) if the uri is not contained.
    194         // Otherwise the performance is O(n), but this is acceptable as we will
    195         // most often call this to find an element at the beginning of the list.
    196         return mImages.indexOf(uri);
    197     }
    198 
    199     @Override
    200     public boolean undoDataRemoval() {
    201         if (mLocalDataToDelete == null) {
    202             return false;
    203         }
    204         LocalData d = mLocalDataToDelete;
    205         mLocalDataToDelete = null;
    206         insertData(d);
    207         return true;
    208     }
    209 
    210     @Override
    211     public boolean executeDeletion() {
    212         if (mLocalDataToDelete == null) {
    213             return false;
    214         }
    215 
    216         DeletionTask task = new DeletionTask();
    217         task.execute(mLocalDataToDelete);
    218         mLocalDataToDelete = null;
    219         return true;
    220     }
    221 
    222     @Override
    223     public void flush() {
    224         replaceData(new LocalDataList());
    225     }
    226 
    227     @Override
    228     public void refresh(Uri uri) {
    229         final int pos = findDataByContentUri(uri);
    230         if (pos == -1) {
    231             return;
    232         }
    233 
    234         LocalData data = mImages.get(pos);
    235         LocalData refreshedData = data.refresh(mContext);
    236 
    237         // Refresh failed. Probably removed already.
    238         if (refreshedData == null && mListener != null) {
    239             mListener.onDataRemoved(pos, data);
    240             return;
    241         }
    242         updateData(pos, refreshedData);
    243     }
    244 
    245     @Override
    246     public void updateData(final int pos, LocalData data) {
    247         mImages.set(pos, data);
    248         if (mListener != null) {
    249             mListener.onDataUpdated(new UpdateReporter() {
    250                 @Override
    251                 public boolean isDataRemoved(int dataID) {
    252                     return false;
    253                 }
    254 
    255                 @Override
    256                 public boolean isDataUpdated(int dataID) {
    257                     return (dataID == pos);
    258                 }
    259             });
    260         }
    261     }
    262 
    263     private void insertData(LocalData data) {
    264         // Since this function is mostly for adding the newest data,
    265         // a simple linear search should yield the best performance over a
    266         // binary search.
    267         int pos = 0;
    268         Comparator<LocalData> comp = new LocalData.NewestFirstComparator();
    269         for (; pos < mImages.size()
    270                 && comp.compare(data, mImages.get(pos)) > 0; pos++) {
    271             ;
    272         }
    273         mImages.add(pos, data);
    274         if (mListener != null) {
    275             mListener.onDataInserted(pos, data);
    276         }
    277     }
    278 
    279     /** Update all the data */
    280     private void replaceData(LocalDataList list) {
    281         if (list.size() == 0 && mImages.size() == 0) {
    282             return;
    283         }
    284         mImages = list;
    285         if (mListener != null) {
    286             mListener.onDataLoaded();
    287         }
    288     }
    289 
    290     @Override
    291     public List<AsyncTask> preloadItems(List<Integer> items) {
    292         List<AsyncTask> result = new ArrayList<AsyncTask>();
    293         for (Integer id : items) {
    294             if (!isMetadataUpdated(id)) {
    295                 result.add(updateMetadata(id));
    296             }
    297         }
    298         return result;
    299     }
    300 
    301     @Override
    302     public void cancelItems(List<AsyncTask> loadTokens) {
    303         for (AsyncTask asyncTask : loadTokens) {
    304             if (asyncTask != null) {
    305                 asyncTask.cancel(false);
    306             }
    307         }
    308     }
    309 
    310     @Override
    311     public List<Integer> getItemsInRange(int startPosition, int endPosition) {
    312         List<Integer> result = new ArrayList<Integer>();
    313         for (int i = Math.max(0, startPosition); i < endPosition; i++) {
    314             result.add(i);
    315         }
    316         return result;
    317     }
    318 
    319     @Override
    320     public int getCount() {
    321         return getTotalNumber();
    322     }
    323 
    324     private class LoadNewPhotosTask extends AsyncTask<ContentResolver, Void, List<LocalData>> {
    325 
    326         private final long mMinPhotoId;
    327 
    328         public LoadNewPhotosTask(long lastPhotoId) {
    329             mMinPhotoId = lastPhotoId;
    330         }
    331 
    332         /**
    333          * Loads any new photos added to our storage directory since our last query.
    334          * @param contentResolvers {@link android.content.ContentResolver} to load data.
    335          * @return An {@link java.util.ArrayList} containing any new data.
    336          */
    337         @Override
    338         protected List<LocalData> doInBackground(ContentResolver... contentResolvers) {
    339             if (mMinPhotoId != LocalMediaData.QUERY_ALL_MEDIA_ID) {
    340                 final ContentResolver cr = contentResolvers[0];
    341                 return LocalMediaData.PhotoData.query(cr, LocalMediaData.PhotoData.CONTENT_URI,
    342                         mMinPhotoId);
    343             }
    344             return new ArrayList<LocalData>(0);
    345         }
    346 
    347         @Override
    348         protected void onPostExecute(List<LocalData> newPhotoData) {
    349             if (!newPhotoData.isEmpty()) {
    350                 LocalData newestPhoto = newPhotoData.get(0);
    351                 // We may overlap with another load task or a query task, in which case we want
    352                 // to be sure we never decrement the oldest seen id.
    353                 mLastPhotoId = Math.max(mLastPhotoId, newestPhoto.getContentId());
    354             }
    355             // We may add data that is already present, but if we do, it will be deduped in addData.
    356             // addData does not dedupe session items, so we ignore them here
    357             for (LocalData localData : newPhotoData) {
    358                 Uri sessionUri = Storage.getSessionUriFromContentUri(localData.getUri());
    359                 if (sessionUri == null) {
    360                     addData(localData);
    361                 }
    362             }
    363         }
    364     }
    365 
    366     private class QueryTaskResult {
    367         public LocalDataList mLocalDataList;
    368         public long mLastPhotoId;
    369 
    370         public QueryTaskResult(LocalDataList localDataList, long lastPhotoId) {
    371             mLocalDataList = localDataList;
    372             mLastPhotoId = lastPhotoId;
    373         }
    374     }
    375 
    376     private class QueryTask extends AsyncTask<Context, Void, QueryTaskResult> {
    377         // The maximum number of data to load metadata for in a single task.
    378         private static final int MAX_METADATA = 5;
    379 
    380         private final Callback<Void> mDoneCallback;
    381 
    382         public QueryTask(Callback<Void> doneCallback) {
    383             mDoneCallback = doneCallback;
    384         }
    385 
    386         /**
    387          * Loads all the photo and video data in the camera folder in background
    388          * and combine them into one single list.
    389          *
    390          * @param contexts {@link Context} to load all the data.
    391          * @return An {@link com.android.camera.data.CameraDataAdapter.QueryTaskResult} containing
    392          *  all loaded data and the highest photo id in the dataset.
    393          */
    394         @Override
    395         protected QueryTaskResult doInBackground(Context... contexts) {
    396             final Context context = contexts[0];
    397             final ContentResolver cr = context.getContentResolver();
    398             LocalDataList l = new LocalDataList();
    399             // Photos
    400             List<LocalData> photoData = LocalMediaData.PhotoData.query(cr,
    401                     LocalMediaData.PhotoData.CONTENT_URI, LocalMediaData.QUERY_ALL_MEDIA_ID);
    402             List<LocalData> videoData = LocalMediaData.VideoData.query(cr,
    403                     LocalMediaData.VideoData.CONTENT_URI, LocalMediaData.QUERY_ALL_MEDIA_ID);
    404 
    405             long lastPhotoId = LocalMediaData.QUERY_ALL_MEDIA_ID;
    406             if (!photoData.isEmpty()) {
    407                 lastPhotoId = photoData.get(0).getContentId();
    408             }
    409 
    410             l.addAll(photoData);
    411             l.addAll(videoData);
    412             l.sort(new LocalData.NewestFirstComparator());
    413 
    414             // Load enough metadata so it's already loaded when we open the filmstrip.
    415             for (int i = 0; i < MAX_METADATA && i < l.size(); i++) {
    416                 LocalData data = l.get(i);
    417                 MetadataLoader.loadMetadata(context, data);
    418             }
    419             return new QueryTaskResult(l, lastPhotoId);
    420         }
    421 
    422         @Override
    423         protected void onPostExecute(QueryTaskResult result) {
    424             // Since we're wiping away all of our data, we should always replace any existing last
    425             // photo id with the new one we just obtained so it matches the data we're showing.
    426             mLastPhotoId = result.mLastPhotoId;
    427             replaceData(result.mLocalDataList);
    428             if (mDoneCallback != null) {
    429                 mDoneCallback.onCallback(null);
    430             }
    431             // Now check for any photos added since this task was kicked off
    432             LoadNewPhotosTask ltask = new LoadNewPhotosTask(mLastPhotoId);
    433             ltask.execute(mContext.getContentResolver());
    434         }
    435     }
    436 
    437     private class DeletionTask extends AsyncTask<LocalData, Void, Void> {
    438         @Override
    439         protected Void doInBackground(LocalData... data) {
    440             for (int i = 0; i < data.length; i++) {
    441                 if (!data[i].isDataActionSupported(LocalData.DATA_ACTION_DELETE)) {
    442                     Log.v(TAG, "Deletion is not supported:" + data[i]);
    443                     continue;
    444                 }
    445                 data[i].delete(mContext);
    446             }
    447             return null;
    448         }
    449     }
    450 
    451     private class MetadataUpdateTask extends AsyncTask<Integer, Void, List<Integer> > {
    452         @Override
    453         protected List<Integer> doInBackground(Integer... dataId) {
    454             List<Integer> updatedList = new ArrayList<Integer>();
    455             for (Integer id : dataId) {
    456                 if (id < 0 || id >= mImages.size()) {
    457                     continue;
    458                 }
    459                 final LocalData data = mImages.get(id);
    460                 if (MetadataLoader.loadMetadata(mContext, data)) {
    461                     updatedList.add(id);
    462                 }
    463             }
    464             return updatedList;
    465         }
    466 
    467         @Override
    468         protected void onPostExecute(final List<Integer> updatedData) {
    469             // Since the metadata will affect the width and height of the data
    470             // if it's a video, we need to notify the DataAdapter listener
    471             // because ImageData.getWidth() and ImageData.getHeight() now may
    472             // return different values due to the metadata.
    473             if (mListener != null) {
    474                 mListener.onDataUpdated(new UpdateReporter() {
    475                     @Override
    476                     public boolean isDataRemoved(int dataID) {
    477                         return false;
    478                     }
    479 
    480                     @Override
    481                     public boolean isDataUpdated(int dataID) {
    482                         return updatedData.contains(dataID);
    483                     }
    484                 });
    485             }
    486             if (mLocalDataListener == null) {
    487                 return;
    488             }
    489             mLocalDataListener.onMetadataUpdated(updatedData);
    490         }
    491     }
    492 }
    493