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