1/*
2 * Copyright (C) 2012 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 */
16package com.android.dreams.phototable;
17
18import android.content.Context;
19import android.content.SharedPreferences;
20import android.database.Cursor;
21import android.net.Uri;
22import android.provider.MediaStore;
23
24import java.io.FileInputStream;
25import java.io.InputStream;
26import java.util.Collection;
27import java.util.HashMap;
28import java.util.LinkedList;
29import java.util.Set;
30
31/**
32 * Loads images from the local store.
33 */
34public class LocalSource extends CursorPhotoSource {
35    private static final String TAG = "PhotoTable.LocalSource";
36
37    private final String mUnknownAlbumName;
38    private final String mLocalSourceName;
39    private Set<String> mFoundAlbumIds;
40    private int mLastPosition;
41
42    public LocalSource(Context context, SharedPreferences settings) {
43        super(context, settings);
44        mLocalSourceName = mResources.getString(R.string.local_source_name, "Photos on Device");
45        mUnknownAlbumName = mResources.getString(R.string.unknown_album_name, "Unknown");
46        mSourceName = TAG;
47        mLastPosition = INVALID;
48        fillQueue();
49    }
50
51    private Set<String> getFoundAlbums() {
52        if (mFoundAlbumIds == null) {
53            findAlbums();
54        }
55        return mFoundAlbumIds;
56    }
57
58    @Override
59    public Collection<AlbumData> findAlbums() {
60        log(TAG, "finding albums");
61        HashMap<String, AlbumData> foundAlbums = new HashMap<String, AlbumData>();
62        findAlbums(false, foundAlbums);
63        findAlbums(true, foundAlbums);
64
65        log(TAG, "found " + foundAlbums.size() + " items.");
66        mFoundAlbumIds = foundAlbums.keySet();
67        return foundAlbums.values();
68    }
69
70    public void findAlbums(boolean internal, HashMap<String, AlbumData> foundAlbums) {
71        Uri uri = internal ? MediaStore.Images.Media.INTERNAL_CONTENT_URI
72            : MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
73        String[] projection = {MediaStore.Images.Media.DATA, MediaStore.Images.Media.BUCKET_ID,
74                MediaStore.Images.Media.BUCKET_DISPLAY_NAME, MediaStore.Images.Media.DATE_TAKEN};
75        // This is a horrible hack that closes the where clause and injects a grouping clause.
76        Cursor cursor = mResolver.query(uri, projection, null, null, null);
77        if (cursor != null) {
78            cursor.moveToPosition(-1);
79
80            int dataIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA);
81            int bucketIndex = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID);
82            int nameIndex = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME);
83            int updatedIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN);
84
85            if (bucketIndex < 0) {
86                log(TAG, "can't find the ID column!");
87            } else {
88                while (cursor.moveToNext()) {
89                    String id = constructId(internal, cursor.getString(bucketIndex));
90                    AlbumData data = foundAlbums.get(id);
91                    if (foundAlbums.get(id) == null) {
92                        data = new AlbumData();
93                        data.id = id;
94                        data.account = mLocalSourceName;
95
96                        if (dataIndex >= 0) {
97                            data.thumbnailUrl = cursor.getString(dataIndex);
98                        }
99
100                        if (nameIndex >= 0) {
101                            data.title = cursor.getString(nameIndex);
102                        } else {
103                            data.title = mUnknownAlbumName;
104                        }
105
106                        log(TAG, data.title + " found");
107                        foundAlbums.put(id, data);
108                    }
109                    if (updatedIndex >= 0) {
110                        long updated = cursor.getLong(updatedIndex);
111                        data.updated = (data.updated == 0 ?
112                                        updated :
113                                        Math.min(data.updated, updated));
114                    }
115                }
116            }
117            cursor.close();
118        }
119    }
120
121    public static String constructId(boolean internal, String bucketId) {
122        return TAG + ":" + bucketId + (internal ? ":i" : "");
123    }
124
125    @Override
126    protected void openCursor(ImageData data) {
127        log(TAG, "opening single album");
128
129        String[] projection = {MediaStore.Images.Media.DATA, MediaStore.Images.Media.ORIENTATION,
130                MediaStore.Images.Media.BUCKET_ID, MediaStore.Images.Media.BUCKET_DISPLAY_NAME};
131        String selection = MediaStore.Images.Media.BUCKET_ID + " = '" + data.albumId + "'";
132
133        data.cursor = mResolver.query(data.uri, projection, selection, null, null);
134    }
135
136    @Override
137    protected void findPosition(ImageData data) {
138        if (data.position == -1) {
139            if (data.cursor == null) {
140                openCursor(data);
141            }
142            if (data.cursor != null) {
143                int dataIndex = data.cursor.getColumnIndex(MediaStore.Images.Media.DATA);
144                data.cursor.moveToPosition(-1);
145                while (data.position == -1 && data.cursor.moveToNext()) {
146                    String url = data.cursor.getString(dataIndex);
147                    if (url != null && url.equals(data.url)) {
148                        data.position = data.cursor.getPosition();
149                    }
150                }
151                if (data.position == -1) {
152                    // oops!  The image isn't in this album. How did we get here?
153                    data.position = INVALID;
154                }
155            }
156        }
157    }
158
159    @Override
160    protected ImageData unpackImageData(Cursor cursor, ImageData data) {
161        if (data == null) {
162            data = new ImageData();
163        }
164        int dataIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA);
165        int orientationIndex = cursor.getColumnIndex(MediaStore.Images.Media.ORIENTATION);
166        int bucketIndex = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID);
167
168        data.url = cursor.getString(dataIndex);
169        data.albumId = cursor.getString(bucketIndex);
170        data.position = UNINITIALIZED;
171        data.cursor = null;
172        data.orientation = cursor.getInt(orientationIndex);
173
174        return data;
175    }
176
177    @Override
178    protected Collection<ImageData> findImages(int howMany) {
179        log(TAG, "finding images");
180        LinkedList<ImageData> foundImages = new LinkedList<ImageData>();
181        boolean internalFirst = mRNG.nextInt(2) == 0;  // filp a coin to be fair
182        findImages(internalFirst, howMany, foundImages);
183        findImages(!internalFirst, howMany - foundImages.size(), foundImages);
184        log(TAG, "found " + foundImages.size() + " items.");
185        return foundImages;
186    }
187
188    protected void findImages(boolean internal, int howMany, LinkedList<ImageData> foundImages ) {
189        Uri uri = internal ? MediaStore.Images.Media.INTERNAL_CONTENT_URI
190            : MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
191        String[] projection = {MediaStore.Images.Media.DATA, MediaStore.Images.Media.ORIENTATION,
192                MediaStore.Images.Media.BUCKET_ID, MediaStore.Images.Media.BUCKET_DISPLAY_NAME};
193        String selection = "";
194        for (String id : getFoundAlbums()) {
195            if (isInternalId(id) == internal && mSettings.isAlbumEnabled(id)) {
196                String[] parts = id.split(":");
197                if (parts.length > 1) {
198                    if (selection.length() > 0) {
199                        selection += " OR ";
200                    }
201                    selection += MediaStore.Images.Media.BUCKET_ID + " = '" + parts[1] + "'";
202                }
203            }
204        }
205        if (selection.isEmpty()) {
206            return;
207        }
208        Cursor cursor = mResolver.query(uri, projection, selection, null, null);
209        if (cursor != null) {
210            int dataIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA);
211
212            if (cursor.getCount() > howMany && mLastPosition == INVALID) {
213                mLastPosition = pickRandomStart(cursor.getCount(), howMany);
214            }
215            cursor.moveToPosition(mLastPosition);
216
217            if (dataIndex < 0) {
218                log(TAG, "can't find the DATA column!");
219            } else {
220                while (foundImages.size() < howMany && cursor.moveToNext()) {
221                    ImageData data = unpackImageData(cursor, null);
222                    data.uri = uri;
223                    foundImages.offer(data);
224                    mLastPosition = cursor.getPosition();
225                }
226                if (cursor.isAfterLast()) {
227                    mLastPosition = -1;
228                }
229                if (cursor.isBeforeFirst()) {
230                    mLastPosition = INVALID;
231                }
232            }
233
234            cursor.close();
235        }
236    }
237
238    private boolean isInternalId(String id) {
239        return id.endsWith("i");
240    }
241
242    @Override
243    protected InputStream getStream(ImageData data, int longSide) {
244        FileInputStream fis = null;
245        try {
246            log(TAG, "opening:" + data.url);
247            fis = new FileInputStream(data.url);
248        } catch (Exception ex) {
249            log(TAG, ex.toString());
250            fis = null;
251        }
252
253        return (InputStream) fis;
254    }
255}
256
257