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