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
17package com.android.gallery3d.gadget;
18
19import android.content.ContentResolver;
20import android.content.Context;
21import android.database.ContentObserver;
22import android.database.Cursor;
23import android.graphics.Bitmap;
24import android.net.Uri;
25import android.os.Environment;
26import android.os.Handler;
27import android.provider.MediaStore.Images.Media;
28
29import com.android.gallery3d.app.GalleryApp;
30import com.android.gallery3d.common.Utils;
31import com.android.gallery3d.data.ContentListener;
32import com.android.gallery3d.data.DataManager;
33import com.android.gallery3d.data.MediaItem;
34import com.android.gallery3d.data.Path;
35import com.android.gallery3d.util.GalleryUtils;
36
37import java.util.ArrayList;
38import java.util.Arrays;
39import java.util.HashSet;
40import java.util.Random;
41
42public 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
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
82    public void close() {
83        mContext.getContentResolver().unregisterContentObserver(mContentObserver);
84    }
85
86    @Override
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
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
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
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
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
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
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     */
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
202    public void setContentListener(ContentListener listener) {
203        mContentListener = listener;
204    }
205}
206