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