1/*
2 * Copyright (C) 2010 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.gallery3d.data;
18
19import android.content.ContentResolver;
20import android.content.res.Resources;
21import android.database.Cursor;
22import android.net.Uri;
23import android.provider.MediaStore;
24import android.provider.MediaStore.Images;
25import android.provider.MediaStore.Images.ImageColumns;
26import android.provider.MediaStore.Video;
27import android.provider.MediaStore.Video.VideoColumns;
28
29import com.android.gallery3d.R;
30import com.android.gallery3d.app.GalleryApp;
31import com.android.gallery3d.common.Utils;
32import com.android.gallery3d.util.GalleryUtils;
33import com.android.gallery3d.util.MediaSetUtils;
34
35import java.util.ArrayList;
36
37// LocalAlbumSet lists all media items in one bucket on local storage.
38// The media items need to be all images or all videos, but not both.
39public class LocalAlbum extends MediaSet {
40    private static final String TAG = "LocalAlbum";
41    private static final String[] COUNT_PROJECTION = { "count(*)" };
42
43    private static final int INVALID_COUNT = -1;
44    private final String mWhereClause;
45    private final String mOrderClause;
46    private final Uri mBaseUri;
47    private final String[] mProjection;
48
49    private final GalleryApp mApplication;
50    private final ContentResolver mResolver;
51    private final int mBucketId;
52    private final String mName;
53    private final boolean mIsImage;
54    private final ChangeNotifier mNotifier;
55    private final Path mItemPath;
56    private int mCachedCount = INVALID_COUNT;
57
58    public LocalAlbum(Path path, GalleryApp application, int bucketId,
59            boolean isImage, String name) {
60        super(path, nextVersionNumber());
61        mApplication = application;
62        mResolver = application.getContentResolver();
63        mBucketId = bucketId;
64        mName = getLocalizedName(application.getResources(), bucketId, name);
65        mIsImage = isImage;
66
67        if (isImage) {
68            mWhereClause = ImageColumns.BUCKET_ID + " = ?";
69            mOrderClause = ImageColumns.DATE_TAKEN + " DESC, "
70                    + ImageColumns._ID + " DESC";
71            mBaseUri = Images.Media.EXTERNAL_CONTENT_URI;
72            mProjection = LocalImage.PROJECTION;
73            mItemPath = LocalImage.ITEM_PATH;
74        } else {
75            mWhereClause = VideoColumns.BUCKET_ID + " = ?";
76            mOrderClause = VideoColumns.DATE_TAKEN + " DESC, "
77                    + VideoColumns._ID + " DESC";
78            mBaseUri = Video.Media.EXTERNAL_CONTENT_URI;
79            mProjection = LocalVideo.PROJECTION;
80            mItemPath = LocalVideo.ITEM_PATH;
81        }
82
83        mNotifier = new ChangeNotifier(this, mBaseUri, application);
84    }
85
86    public LocalAlbum(Path path, GalleryApp application, int bucketId,
87            boolean isImage) {
88        this(path, application, bucketId, isImage,
89                BucketHelper.getBucketName(
90                application.getContentResolver(), bucketId));
91    }
92
93    @Override
94    public boolean isCameraRoll() {
95        return mBucketId == MediaSetUtils.CAMERA_BUCKET_ID;
96    }
97
98    @Override
99    public Uri getContentUri() {
100        if (mIsImage) {
101            return MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon()
102                    .appendQueryParameter(LocalSource.KEY_BUCKET_ID,
103                            String.valueOf(mBucketId)).build();
104        } else {
105            return MediaStore.Video.Media.EXTERNAL_CONTENT_URI.buildUpon()
106                    .appendQueryParameter(LocalSource.KEY_BUCKET_ID,
107                            String.valueOf(mBucketId)).build();
108        }
109    }
110
111    @Override
112    public ArrayList<MediaItem> getMediaItem(int start, int count) {
113        DataManager dataManager = mApplication.getDataManager();
114        Uri uri = mBaseUri.buildUpon()
115                .appendQueryParameter("limit", start + "," + count).build();
116        ArrayList<MediaItem> list = new ArrayList<MediaItem>();
117        GalleryUtils.assertNotInRenderThread();
118        Cursor cursor = mResolver.query(
119                uri, mProjection, mWhereClause,
120                new String[]{String.valueOf(mBucketId)},
121                mOrderClause);
122        if (cursor == null) {
123            Log.w(TAG, "query fail: " + uri);
124            return list;
125        }
126
127        try {
128            while (cursor.moveToNext()) {
129                int id = cursor.getInt(0);  // _id must be in the first column
130                Path childPath = mItemPath.getChild(id);
131                MediaItem item = loadOrUpdateItem(childPath, cursor,
132                        dataManager, mApplication, mIsImage);
133                list.add(item);
134            }
135        } finally {
136            cursor.close();
137        }
138        return list;
139    }
140
141    private static MediaItem loadOrUpdateItem(Path path, Cursor cursor,
142            DataManager dataManager, GalleryApp app, boolean isImage) {
143        synchronized (DataManager.LOCK) {
144            LocalMediaItem item = (LocalMediaItem) dataManager.peekMediaObject(path);
145            if (item == null) {
146                if (isImage) {
147                    item = new LocalImage(path, app, cursor);
148                } else {
149                    item = new LocalVideo(path, app, cursor);
150                }
151            } else {
152                item.updateContent(cursor);
153            }
154            return item;
155        }
156    }
157
158    // The pids array are sorted by the (path) id.
159    public static MediaItem[] getMediaItemById(
160            GalleryApp application, boolean isImage, ArrayList<Integer> ids) {
161        // get the lower and upper bound of (path) id
162        MediaItem[] result = new MediaItem[ids.size()];
163        if (ids.isEmpty()) return result;
164        int idLow = ids.get(0);
165        int idHigh = ids.get(ids.size() - 1);
166
167        // prepare the query parameters
168        Uri baseUri;
169        String[] projection;
170        Path itemPath;
171        if (isImage) {
172            baseUri = Images.Media.EXTERNAL_CONTENT_URI;
173            projection = LocalImage.PROJECTION;
174            itemPath = LocalImage.ITEM_PATH;
175        } else {
176            baseUri = Video.Media.EXTERNAL_CONTENT_URI;
177            projection = LocalVideo.PROJECTION;
178            itemPath = LocalVideo.ITEM_PATH;
179        }
180
181        ContentResolver resolver = application.getContentResolver();
182        DataManager dataManager = application.getDataManager();
183        Cursor cursor = resolver.query(baseUri, projection, "_id BETWEEN ? AND ?",
184                new String[]{String.valueOf(idLow), String.valueOf(idHigh)},
185                "_id");
186        if (cursor == null) {
187            Log.w(TAG, "query fail" + baseUri);
188            return result;
189        }
190        try {
191            int n = ids.size();
192            int i = 0;
193
194            while (i < n && cursor.moveToNext()) {
195                int id = cursor.getInt(0);  // _id must be in the first column
196
197                // Match id with the one on the ids list.
198                if (ids.get(i) > id) {
199                    continue;
200                }
201
202                while (ids.get(i) < id) {
203                    if (++i >= n) {
204                        return result;
205                    }
206                }
207
208                Path childPath = itemPath.getChild(id);
209                MediaItem item = loadOrUpdateItem(childPath, cursor, dataManager,
210                        application, isImage);
211                result[i] = item;
212                ++i;
213            }
214            return result;
215        } finally {
216            cursor.close();
217        }
218    }
219
220    public static Cursor getItemCursor(ContentResolver resolver, Uri uri,
221            String[] projection, int id) {
222        return resolver.query(uri, projection, "_id=?",
223                new String[]{String.valueOf(id)}, null);
224    }
225
226    @Override
227    public int getMediaItemCount() {
228        if (mCachedCount == INVALID_COUNT) {
229            Cursor cursor = mResolver.query(
230                    mBaseUri, COUNT_PROJECTION, mWhereClause,
231                    new String[]{String.valueOf(mBucketId)}, null);
232            if (cursor == null) {
233                Log.w(TAG, "query fail");
234                return 0;
235            }
236            try {
237                Utils.assertTrue(cursor.moveToNext());
238                mCachedCount = cursor.getInt(0);
239            } finally {
240                cursor.close();
241            }
242        }
243        return mCachedCount;
244    }
245
246    @Override
247    public String getName() {
248        return mName;
249    }
250
251    @Override
252    public long reload() {
253        if (mNotifier.isDirty()) {
254            mDataVersion = nextVersionNumber();
255            mCachedCount = INVALID_COUNT;
256        }
257        return mDataVersion;
258    }
259
260    @Override
261    public int getSupportedOperations() {
262        return SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_INFO;
263    }
264
265    @Override
266    public void delete() {
267        GalleryUtils.assertNotInRenderThread();
268        mResolver.delete(mBaseUri, mWhereClause,
269                new String[]{String.valueOf(mBucketId)});
270    }
271
272    @Override
273    public boolean isLeafAlbum() {
274        return true;
275    }
276
277    public static String getLocalizedName(Resources res, int bucketId,
278            String name) {
279        if (bucketId == MediaSetUtils.CAMERA_BUCKET_ID) {
280            return res.getString(R.string.folder_camera);
281        } else if (bucketId == MediaSetUtils.DOWNLOAD_BUCKET_ID) {
282            return res.getString(R.string.folder_download);
283        } else if (bucketId == MediaSetUtils.IMPORTED_BUCKET_ID) {
284            return res.getString(R.string.folder_imported);
285        } else if (bucketId == MediaSetUtils.SNAPSHOT_BUCKET_ID) {
286            return res.getString(R.string.folder_screenshot);
287        } else if (bucketId == MediaSetUtils.EDITED_ONLINE_PHOTOS_BUCKET_ID) {
288            return res.getString(R.string.folder_edited_online_photos);
289        } else {
290            return name;
291        }
292    }
293}
294