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 */
16
17package com.android.gallery3d.data;
18
19import android.content.Context;
20import android.database.Cursor;
21import android.net.Uri;
22import android.provider.MediaStore.Images;
23import android.provider.MediaStore.MediaColumns;
24import android.provider.MediaStore.Video;
25
26import com.android.gallery3d.app.GalleryApp;
27import com.android.gallery3d.app.StitchingChangeListener;
28import com.android.gallery3d.util.MediaSetUtils;
29
30import java.util.ArrayList;
31
32// This class lists all media items added by the client.
33public class SecureAlbum extends MediaSet implements StitchingChangeListener {
34    @SuppressWarnings("unused")
35    private static final String TAG = "SecureAlbum";
36    private static final String[] PROJECTION = {MediaColumns._ID};
37    private int mMinImageId = Integer.MAX_VALUE; // the smallest id of images
38    private int mMaxImageId = Integer.MIN_VALUE; // the biggest id in images
39    private int mMinVideoId = Integer.MAX_VALUE; // the smallest id of videos
40    private int mMaxVideoId = Integer.MIN_VALUE; // the biggest id of videos
41    // All the media items added by the client.
42    private ArrayList<Path> mAllItems = new ArrayList<Path>();
43    // The types of items in mAllItems. True is video and false is image.
44    private ArrayList<Boolean> mAllItemTypes = new ArrayList<Boolean>();
45    private ArrayList<Path> mExistingItems = new ArrayList<Path>();
46    private Context mContext;
47    private DataManager mDataManager;
48    private static final Uri[] mWatchUris =
49        {Images.Media.EXTERNAL_CONTENT_URI, Video.Media.EXTERNAL_CONTENT_URI};
50    private final ChangeNotifier mNotifier;
51    // A placeholder image in the end of secure album. When it is tapped, it
52    // will take the user to the lock screen.
53    private MediaItem mUnlockItem;
54    private boolean mShowUnlockItem;
55
56    public SecureAlbum(Path path, GalleryApp application, MediaItem unlock) {
57        super(path, nextVersionNumber());
58        mContext = application.getAndroidContext();
59        mDataManager = application.getDataManager();
60        mNotifier = new ChangeNotifier(this, mWatchUris, application);
61        mUnlockItem = unlock;
62        mShowUnlockItem = (!isCameraBucketEmpty(Images.Media.EXTERNAL_CONTENT_URI)
63                || !isCameraBucketEmpty(Video.Media.EXTERNAL_CONTENT_URI));
64    }
65
66    public void addMediaItem(boolean isVideo, int id) {
67        Path pathBase;
68        if (isVideo) {
69            pathBase = LocalVideo.ITEM_PATH;
70            mMinVideoId = Math.min(mMinVideoId, id);
71            mMaxVideoId = Math.max(mMaxVideoId, id);
72        } else {
73            pathBase = LocalImage.ITEM_PATH;
74            mMinImageId = Math.min(mMinImageId, id);
75            mMaxImageId = Math.max(mMaxImageId, id);
76        }
77        Path path = pathBase.getChild(id);
78        if (!mAllItems.contains(path)) {
79            mAllItems.add(path);
80            mAllItemTypes.add(isVideo);
81            mNotifier.fakeChange();
82        }
83    }
84
85    // The sequence is stitching items, local media items, and unlock image.
86    @Override
87    public ArrayList<MediaItem> getMediaItem(int start, int count) {
88        int existingCount = mExistingItems.size();
89        if (start >= existingCount + 1) {
90            return new ArrayList<MediaItem>();
91        }
92
93        // Add paths of requested stitching items.
94        int end = Math.min(start + count, existingCount);
95        ArrayList<Path> subset = new ArrayList<Path>(mExistingItems.subList(start, end));
96
97        // Convert paths to media items.
98        final MediaItem[] buf = new MediaItem[end - start];
99        ItemConsumer consumer = new ItemConsumer() {
100            @Override
101            public void consume(int index, MediaItem item) {
102                buf[index] = item;
103            }
104        };
105        mDataManager.mapMediaItems(subset, consumer, 0);
106        ArrayList<MediaItem> result = new ArrayList<MediaItem>(end - start);
107        for (int i = 0; i < buf.length; i++) {
108            result.add(buf[i]);
109        }
110        if (mShowUnlockItem) result.add(mUnlockItem);
111        return result;
112    }
113
114    @Override
115    public int getMediaItemCount() {
116        return (mExistingItems.size() + (mShowUnlockItem ? 1 : 0));
117    }
118
119    @Override
120    public String getName() {
121        return "secure";
122    }
123
124    @Override
125    public long reload() {
126        if (mNotifier.isDirty()) {
127            mDataVersion = nextVersionNumber();
128            updateExistingItems();
129        }
130        return mDataVersion;
131    }
132
133    private ArrayList<Integer> queryExistingIds(Uri uri, int minId, int maxId) {
134        ArrayList<Integer> ids = new ArrayList<Integer>();
135        if (minId == Integer.MAX_VALUE || maxId == Integer.MIN_VALUE) return ids;
136
137        String[] selectionArgs = {String.valueOf(minId), String.valueOf(maxId)};
138        Cursor cursor = mContext.getContentResolver().query(uri, PROJECTION,
139                "_id BETWEEN ? AND ?", selectionArgs, null);
140        if (cursor == null) return ids;
141        try {
142            while (cursor.moveToNext()) {
143                ids.add(cursor.getInt(0));
144            }
145        } finally {
146            cursor.close();
147        }
148        return ids;
149    }
150
151    private boolean isCameraBucketEmpty(Uri baseUri) {
152        Uri uri = baseUri.buildUpon()
153                .appendQueryParameter("limit", "1").build();
154        String[] selection = {String.valueOf(MediaSetUtils.CAMERA_BUCKET_ID)};
155        Cursor cursor = mContext.getContentResolver().query(uri, PROJECTION,
156                "bucket_id = ?", selection, null);
157        if (cursor == null) return true;
158        try {
159            return (cursor.getCount() == 0);
160        } finally {
161            cursor.close();
162        }
163    }
164
165    private void updateExistingItems() {
166        if (mAllItems.size() == 0) return;
167
168        // Query existing ids.
169        ArrayList<Integer> imageIds = queryExistingIds(
170                Images.Media.EXTERNAL_CONTENT_URI, mMinImageId, mMaxImageId);
171        ArrayList<Integer> videoIds = queryExistingIds(
172                Video.Media.EXTERNAL_CONTENT_URI, mMinVideoId, mMaxVideoId);
173
174        // Construct the existing items list.
175        mExistingItems.clear();
176        for (int i = mAllItems.size() - 1; i >= 0; i--) {
177            Path path = mAllItems.get(i);
178            boolean isVideo = mAllItemTypes.get(i);
179            int id = Integer.parseInt(path.getSuffix());
180            if (isVideo) {
181                if (videoIds.contains(id)) mExistingItems.add(path);
182            } else {
183                if (imageIds.contains(id)) mExistingItems.add(path);
184            }
185        }
186    }
187
188    @Override
189    public boolean isLeafAlbum() {
190        return true;
191    }
192
193    @Override
194    public void onStitchingQueued(Uri uri) {
195        int id = Integer.parseInt(uri.getLastPathSegment());
196        addMediaItem(false, id);
197    }
198
199    @Override
200    public void onStitchingResult(Uri uri) {
201    }
202
203    @Override
204    public void onStitchingProgress(Uri uri, final int progress) {
205    }
206}
207