1 /*
2  * Copyright (C) 2011 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.gallery3d.gadget;
18 
19 import android.content.ContentResolver;
20 import android.content.Context;
21 import android.database.ContentObserver;
22 import android.database.Cursor;
23 import android.graphics.Bitmap;
24 import android.net.Uri;
25 import android.os.Environment;
26 import android.os.Handler;
27 import android.provider.MediaStore.Images.Media;
28 
29 import com.android.gallery3d.app.GalleryApp;
30 import com.android.gallery3d.common.Utils;
31 import com.android.gallery3d.data.ContentListener;
32 import com.android.gallery3d.data.DataManager;
33 import com.android.gallery3d.data.MediaItem;
34 import com.android.gallery3d.data.Path;
35 import com.android.gallery3d.util.GalleryUtils;
36 
37 import java.util.ArrayList;
38 import java.util.Arrays;
39 import java.util.HashSet;
40 import java.util.Random;
41 
42 public class LocalPhotoSource implements WidgetSource {
43 
44     @SuppressWarnings("unused")
45     private static final String TAG = "LocalPhotoSource";
46 
47     private static final int MAX_PHOTO_COUNT = 128;
48 
49     /* Static fields used to query for the correct set of images */
50     private static final Uri CONTENT_URI = Media.EXTERNAL_CONTENT_URI;
51     private static final String DATE_TAKEN = Media.DATE_TAKEN;
52     private static final String[] PROJECTION = {Media._ID};
53     private static final String[] COUNT_PROJECTION = {"count(*)"};
54     /* We don't want to include the download directory */
55     private static final String SELECTION =
56             String.format("%s != %s", Media.BUCKET_ID, getDownloadBucketId());
57     private static final String ORDER = String.format("%s DESC", DATE_TAKEN);
58 
59     private Context mContext;
60     private ArrayList<Long> mPhotos = new ArrayList<Long>();
61     private ContentListener mContentListener;
62     private ContentObserver mContentObserver;
63     private boolean mContentDirty = true;
64     private DataManager mDataManager;
65     private static final Path LOCAL_IMAGE_ROOT = Path.fromString("/local/image/item");
66 
LocalPhotoSource(Context context)67     public LocalPhotoSource(Context context) {
68         mContext = context;
69         mDataManager = ((GalleryApp) context.getApplicationContext()).getDataManager();
70         mContentObserver = new ContentObserver(new Handler()) {
71             @Override
72             public void onChange(boolean selfChange) {
73                 mContentDirty = true;
74                 if (mContentListener != null) mContentListener.onContentDirty();
75             }
76         };
77         mContext.getContentResolver()
78                 .registerContentObserver(CONTENT_URI, true, mContentObserver);
79     }
80 
81     @Override
close()82     public void close() {
83         mContext.getContentResolver().unregisterContentObserver(mContentObserver);
84     }
85 
86     @Override
getContentUri(int index)87     public Uri getContentUri(int index) {
88         if (index < mPhotos.size()) {
89             return CONTENT_URI.buildUpon()
90                     .appendPath(String.valueOf(mPhotos.get(index)))
91                     .build();
92         }
93         return null;
94     }
95 
96     @Override
getImage(int index)97     public Bitmap getImage(int index) {
98         if (index >= mPhotos.size()) return null;
99         long id = mPhotos.get(index);
100         MediaItem image = (MediaItem)
101                 mDataManager.getMediaObject(LOCAL_IMAGE_ROOT.getChild(id));
102         if (image == null) return null;
103 
104         return WidgetUtils.createWidgetBitmap(image);
105     }
106 
getExponentialIndice(int total, int count)107     private int[] getExponentialIndice(int total, int count) {
108         Random random = new Random();
109         if (count > total) count = total;
110         HashSet<Integer> selected = new HashSet<Integer>(count);
111         while (selected.size() < count) {
112             int row = (int)(-Math.log(random.nextDouble()) * total / 2);
113             if (row < total) selected.add(row);
114         }
115         int values[] = new int[count];
116         int index = 0;
117         for (int value : selected) {
118             values[index++] = value;
119         }
120         return values;
121     }
122 
getPhotoCount(ContentResolver resolver)123     private int getPhotoCount(ContentResolver resolver) {
124         Cursor cursor = resolver.query(
125                 CONTENT_URI, COUNT_PROJECTION, SELECTION, null, null);
126         if (cursor == null) return 0;
127         try {
128             Utils.assertTrue(cursor.moveToNext());
129             return cursor.getInt(0);
130         } finally {
131             cursor.close();
132         }
133     }
134 
isContentSound(int totalCount)135     private boolean isContentSound(int totalCount) {
136         if (mPhotos.size() < Math.min(totalCount, MAX_PHOTO_COUNT)) return false;
137         if (mPhotos.size() == 0) return true; // totalCount is also 0
138 
139         StringBuilder builder = new StringBuilder();
140         for (Long imageId : mPhotos) {
141             if (builder.length() > 0) builder.append(",");
142             builder.append(imageId);
143         }
144         Cursor cursor = mContext.getContentResolver().query(
145                 CONTENT_URI, COUNT_PROJECTION,
146                 String.format("%s in (%s)", Media._ID, builder.toString()),
147                 null, null);
148         if (cursor == null) return false;
149         try {
150             Utils.assertTrue(cursor.moveToNext());
151             return cursor.getInt(0) == mPhotos.size();
152         } finally {
153             cursor.close();
154         }
155     }
156 
157     @Override
reload()158     public void reload() {
159         if (!mContentDirty) return;
160         mContentDirty = false;
161 
162         ContentResolver resolver = mContext.getContentResolver();
163         int photoCount = getPhotoCount(resolver);
164         if (isContentSound(photoCount)) return;
165 
166         int choosedIds[] = getExponentialIndice(photoCount, MAX_PHOTO_COUNT);
167         Arrays.sort(choosedIds);
168 
169         mPhotos.clear();
170         Cursor cursor = mContext.getContentResolver().query(
171                 CONTENT_URI, PROJECTION, SELECTION, null, ORDER);
172         if (cursor == null) return;
173         try {
174             for (int index : choosedIds) {
175                 if (cursor.moveToPosition(index)) {
176                     mPhotos.add(cursor.getLong(0));
177                 }
178             }
179         } finally {
180             cursor.close();
181         }
182     }
183 
184     @Override
size()185     public int size() {
186         reload();
187         return mPhotos.size();
188     }
189 
190     /**
191      * Builds the bucket ID for the public external storage Downloads directory
192      * @return the bucket ID
193      */
getDownloadBucketId()194     private static int getDownloadBucketId() {
195         String downloadsPath = Environment
196                 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
197                 .getAbsolutePath();
198         return GalleryUtils.getBucketId(downloadsPath);
199     }
200 
201     @Override
setContentListener(ContentListener listener)202     public void setContentListener(ContentListener listener) {
203         mContentListener = listener;
204     }
205 }
206