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
17package com.android.camera.data;
18
19import android.app.Activity;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.database.Cursor;
23import android.graphics.drawable.Drawable;
24import android.net.Uri;
25import android.os.AsyncTask;
26import android.provider.MediaStore;
27import android.util.Log;
28import android.view.View;
29
30import com.android.camera.Storage;
31import com.android.camera.app.PlaceholderManager;
32import com.android.camera.ui.FilmStripView.ImageData;
33
34import java.util.ArrayList;
35import java.util.Comparator;
36
37/**
38 * A {@link LocalDataAdapter} that provides data in the camera folder.
39 */
40public class CameraDataAdapter implements LocalDataAdapter {
41    private static final String TAG = "CAM_CameraDataAdapter";
42
43    private static final int DEFAULT_DECODE_SIZE = 1600;
44    private static final String[] CAMERA_PATH = { Storage.DIRECTORY + "%" };
45
46    private LocalDataList mImages;
47
48    private Listener mListener;
49    private Drawable mPlaceHolder;
50
51    private int mSuggestedWidth = DEFAULT_DECODE_SIZE;
52    private int mSuggestedHeight = DEFAULT_DECODE_SIZE;
53
54    private LocalData mLocalDataToDelete;
55
56    public CameraDataAdapter(Drawable placeHolder) {
57        mImages = new LocalDataList();
58        mPlaceHolder = placeHolder;
59    }
60
61    @Override
62    public void requestLoad(ContentResolver resolver) {
63        QueryTask qtask = new QueryTask();
64        qtask.execute(resolver);
65    }
66
67    @Override
68    public LocalData getLocalData(int dataID) {
69        if (dataID < 0 || dataID >= mImages.size()) {
70            return null;
71        }
72
73        return mImages.get(dataID);
74    }
75
76    @Override
77    public int getTotalNumber() {
78        return mImages.size();
79    }
80
81    @Override
82    public ImageData getImageData(int id) {
83        return getLocalData(id);
84    }
85
86    @Override
87    public void suggestViewSizeBound(int w, int h) {
88        if (w <= 0 || h <= 0) {
89            mSuggestedWidth  = mSuggestedHeight = DEFAULT_DECODE_SIZE;
90        } else {
91            mSuggestedWidth = (w < DEFAULT_DECODE_SIZE ? w : DEFAULT_DECODE_SIZE);
92            mSuggestedHeight = (h < DEFAULT_DECODE_SIZE ? h : DEFAULT_DECODE_SIZE);
93        }
94    }
95
96    @Override
97    public View getView(Activity activity, int dataID) {
98        if (dataID >= mImages.size() || dataID < 0) {
99            return null;
100        }
101
102        return mImages.get(dataID).getView(
103                activity, mSuggestedWidth, mSuggestedHeight,
104                mPlaceHolder.getConstantState().newDrawable(), this);
105    }
106
107    @Override
108    public void setListener(Listener listener) {
109        mListener = listener;
110        if (mImages != null) {
111            mListener.onDataLoaded();
112        }
113    }
114
115    @Override
116    public boolean canSwipeInFullScreen(int dataID) {
117        if (dataID < mImages.size() && dataID > 0) {
118            return mImages.get(dataID).canSwipeInFullScreen();
119        }
120        return true;
121    }
122
123    @Override
124    public void removeData(Context c, int dataID) {
125        if (dataID >= mImages.size()) return;
126        LocalData d = mImages.remove(dataID);
127        // Delete previously removed data first.
128        executeDeletion(c);
129        mLocalDataToDelete = d;
130        mListener.onDataRemoved(dataID, d);
131    }
132
133    // TODO: put the database query on background thread
134    @Override
135    public void addNewVideo(ContentResolver cr, Uri uri) {
136        Cursor c = cr.query(uri,
137                LocalMediaData.VideoData.QUERY_PROJECTION,
138                MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH,
139                LocalMediaData.VideoData.QUERY_ORDER);
140        if (c == null || !c.moveToFirst()) {
141            return;
142        }
143        int pos = findDataByContentUri(uri);
144        LocalMediaData.VideoData newData = LocalMediaData.VideoData.buildFromCursor(c);
145        if (pos != -1) {
146            // A duplicate one, just do a substitute.
147            updateData(pos, newData);
148        } else {
149            // A new data.
150            insertData(newData);
151        }
152    }
153
154    // TODO: put the database query on background thread
155    @Override
156    public void addNewPhoto(ContentResolver cr, Uri uri) {
157        Cursor c = cr.query(uri,
158                LocalMediaData.PhotoData.QUERY_PROJECTION,
159                MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH,
160                LocalMediaData.PhotoData.QUERY_ORDER);
161        if (c == null || !c.moveToFirst()) {
162            return;
163        }
164        int pos = findDataByContentUri(uri);
165        LocalMediaData.PhotoData newData = LocalMediaData.PhotoData.buildFromCursor(c);
166        if (pos != -1) {
167            // a duplicate one, just do a substitute.
168            Log.v(TAG, "found duplicate photo");
169            updateData(pos, newData);
170        } else {
171            // a new data.
172            insertData(newData);
173        }
174    }
175
176    @Override
177    public int findDataByContentUri(Uri uri) {
178        // LocalDataList will return in O(1) if the uri is not contained.
179        // Otherwise the performance is O(n), but this is acceptable as we will
180        // most often call this to find an element at the beginning of the list.
181        return mImages.indexOf(uri);
182    }
183
184    @Override
185    public boolean undoDataRemoval() {
186        if (mLocalDataToDelete == null) return false;
187        LocalData d = mLocalDataToDelete;
188        mLocalDataToDelete = null;
189        insertData(d);
190        return true;
191    }
192
193    @Override
194    public boolean executeDeletion(Context c) {
195        if (mLocalDataToDelete == null) return false;
196
197        DeletionTask task = new DeletionTask(c);
198        task.execute(mLocalDataToDelete);
199        mLocalDataToDelete = null;
200        return true;
201    }
202
203    @Override
204    public void flush() {
205        replaceData(new LocalDataList());
206    }
207
208    @Override
209    public void refresh(ContentResolver resolver, Uri contentUri) {
210        int pos = findDataByContentUri(contentUri);
211        if (pos == -1) {
212            return;
213        }
214
215        LocalData data = mImages.get(pos);
216        LocalData refreshedData = data.refresh(resolver);
217        if (refreshedData != null) {
218            updateData(pos, refreshedData);
219        }
220    }
221
222    @Override
223    public void updateData(final int pos, LocalData data) {
224        mImages.set(pos, data);
225        if (mListener != null) {
226            mListener.onDataUpdated(new UpdateReporter() {
227                @Override
228                public boolean isDataRemoved(int dataID) {
229                    return false;
230                }
231
232                @Override
233                public boolean isDataUpdated(int dataID) {
234                    return (dataID == pos);
235                }
236            });
237        }
238    }
239
240    @Override
241    public void insertData(LocalData data) {
242        // Since this function is mostly for adding the newest data,
243        // a simple linear search should yield the best performance over a
244        // binary search.
245        int pos = 0;
246        Comparator<LocalData> comp = new LocalData.NewestFirstComparator();
247        for (; pos < mImages.size()
248                && comp.compare(data, mImages.get(pos)) > 0; pos++);
249        mImages.add(pos, data);
250        if (mListener != null) {
251            mListener.onDataInserted(pos, data);
252        }
253    }
254
255    /** Update all the data */
256    private void replaceData(LocalDataList list) {
257        if (list.size() == 0 && mImages.size() == 0) {
258            return;
259        }
260        mImages = list;
261        if (mListener != null) {
262            mListener.onDataLoaded();
263        }
264    }
265
266    private class QueryTask extends AsyncTask<ContentResolver, Void, LocalDataList> {
267
268        /**
269         * Loads all the photo and video data in the camera folder in background
270         * and combine them into one single list.
271         *
272         * @param resolver {@link ContentResolver} to load all the data.
273         * @return An {@link ArrayList} of all loaded data.
274         */
275        @Override
276        protected LocalDataList doInBackground(ContentResolver... resolver) {
277            LocalDataList l = new LocalDataList();
278            // Photos
279            Cursor c = resolver[0].query(
280                    LocalMediaData.PhotoData.CONTENT_URI,
281                    LocalMediaData.PhotoData.QUERY_PROJECTION,
282                    MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH,
283                    LocalMediaData.PhotoData.QUERY_ORDER);
284            if (c != null && c.moveToFirst()) {
285                // build up the list.
286                while (true) {
287                    LocalData data = LocalMediaData.PhotoData.buildFromCursor(c);
288                    if (data != null) {
289                        if (data.getMimeType().equals(PlaceholderManager.PLACEHOLDER_MIME_TYPE)) {
290                            l.add(new InProgressDataWrapper(data, true));
291                        } else {
292                            l.add(data);
293                        }
294                    } else {
295                        Log.e(TAG, "Error loading data:"
296                                + c.getString(LocalMediaData.PhotoData.COL_DATA));
297                    }
298                    if (c.isLast()) {
299                        break;
300                    }
301                    c.moveToNext();
302                }
303            }
304            if (c != null) {
305                c.close();
306            }
307
308            c = resolver[0].query(
309                    LocalMediaData.VideoData.CONTENT_URI,
310                    LocalMediaData.VideoData.QUERY_PROJECTION,
311                    MediaStore.Video.Media.DATA + " like ? ", CAMERA_PATH,
312                    LocalMediaData.VideoData.QUERY_ORDER);
313            if (c != null && c.moveToFirst()) {
314                // build up the list.
315                c.moveToFirst();
316                while (true) {
317                    LocalData data = LocalMediaData.VideoData.buildFromCursor(c);
318                    if (data != null) {
319                        l.add(data);
320                    } else {
321                        Log.e(TAG, "Error loading data:"
322                                + c.getString(LocalMediaData.VideoData.COL_DATA));
323                    }
324                    if (!c.isLast()) {
325                        c.moveToNext();
326                    } else {
327                        break;
328                    }
329                }
330            }
331            if (c != null) {
332                c.close();
333            }
334
335            if (l.size() != 0) {
336                l.sort(new LocalData.NewestFirstComparator());
337            }
338
339            return l;
340        }
341
342        @Override
343        protected void onPostExecute(LocalDataList l) {
344            replaceData(l);
345        }
346    }
347
348    private class DeletionTask extends AsyncTask<LocalData, Void, Void> {
349        Context mContext;
350
351        DeletionTask(Context context) {
352            mContext = context;
353        }
354
355        @Override
356        protected Void doInBackground(LocalData... data) {
357            for (int i = 0; i < data.length; i++) {
358                if (!data[i].isDataActionSupported(LocalData.ACTION_DELETE)) {
359                    Log.v(TAG, "Deletion is not supported:" + data[i]);
360                    continue;
361                }
362                data[i].delete(mContext);
363            }
364            return null;
365        }
366    }
367}
368