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.net.Uri;
20import android.os.Handler;
21import android.provider.MediaStore.Images;
22import android.provider.MediaStore.Video;
23
24import com.android.gallery3d.R;
25import com.android.gallery3d.app.GalleryApp;
26import com.android.gallery3d.data.BucketHelper.BucketEntry;
27import com.android.gallery3d.util.Future;
28import com.android.gallery3d.util.FutureListener;
29import com.android.gallery3d.util.MediaSetUtils;
30import com.android.gallery3d.util.ThreadPool;
31import com.android.gallery3d.util.ThreadPool.JobContext;
32
33import java.util.ArrayList;
34import java.util.Comparator;
35
36// LocalAlbumSet lists all image or video albums in the local storage.
37// The path should be "/local/image", "local/video" or "/local/all"
38public class LocalAlbumSet extends MediaSet
39        implements FutureListener<ArrayList<MediaSet>> {
40    @SuppressWarnings("unused")
41    private static final String TAG = "LocalAlbumSet";
42
43    public static final Path PATH_ALL = Path.fromString("/local/all");
44    public static final Path PATH_IMAGE = Path.fromString("/local/image");
45    public static final Path PATH_VIDEO = Path.fromString("/local/video");
46
47    private static final Uri[] mWatchUris =
48        {Images.Media.EXTERNAL_CONTENT_URI, Video.Media.EXTERNAL_CONTENT_URI};
49
50    private final GalleryApp mApplication;
51    private final int mType;
52    private ArrayList<MediaSet> mAlbums = new ArrayList<MediaSet>();
53    private final ChangeNotifier mNotifier;
54    private final String mName;
55    private final Handler mHandler;
56    private boolean mIsLoading;
57
58    private Future<ArrayList<MediaSet>> mLoadTask;
59    private ArrayList<MediaSet> mLoadBuffer;
60
61    public LocalAlbumSet(Path path, GalleryApp application) {
62        super(path, nextVersionNumber());
63        mApplication = application;
64        mHandler = new Handler(application.getMainLooper());
65        mType = getTypeFromPath(path);
66        mNotifier = new ChangeNotifier(this, mWatchUris, application);
67        mName = application.getResources().getString(
68                R.string.set_label_local_albums);
69    }
70
71    private static int getTypeFromPath(Path path) {
72        String name[] = path.split();
73        if (name.length < 2) {
74            throw new IllegalArgumentException(path.toString());
75        }
76        return getTypeFromString(name[1]);
77    }
78
79    @Override
80    public MediaSet getSubMediaSet(int index) {
81        return mAlbums.get(index);
82    }
83
84    @Override
85    public int getSubMediaSetCount() {
86        return mAlbums.size();
87    }
88
89    @Override
90    public String getName() {
91        return mName;
92    }
93
94    private static int findBucket(BucketEntry entries[], int bucketId) {
95        for (int i = 0, n = entries.length; i < n; ++i) {
96            if (entries[i].bucketId == bucketId) return i;
97        }
98        return -1;
99    }
100
101    private class AlbumsLoader implements ThreadPool.Job<ArrayList<MediaSet>> {
102
103        @Override
104        @SuppressWarnings("unchecked")
105        public ArrayList<MediaSet> run(JobContext jc) {
106            // Note: it will be faster if we only select media_type and bucket_id.
107            //       need to test the performance if that is worth
108            BucketEntry[] entries = BucketHelper.loadBucketEntries(
109                    jc, mApplication.getContentResolver(), mType);
110
111            if (jc.isCancelled()) return null;
112
113            int offset = 0;
114            // Move camera and download bucket to the front, while keeping the
115            // order of others.
116            int index = findBucket(entries, MediaSetUtils.CAMERA_BUCKET_ID);
117            if (index != -1) {
118                circularShiftRight(entries, offset++, index);
119            }
120            index = findBucket(entries, MediaSetUtils.DOWNLOAD_BUCKET_ID);
121            if (index != -1) {
122                circularShiftRight(entries, offset++, index);
123            }
124
125            ArrayList<MediaSet> albums = new ArrayList<MediaSet>();
126            DataManager dataManager = mApplication.getDataManager();
127            for (BucketEntry entry : entries) {
128                MediaSet album = getLocalAlbum(dataManager,
129                        mType, mPath, entry.bucketId, entry.bucketName);
130                albums.add(album);
131            }
132            return albums;
133        }
134    }
135
136    private MediaSet getLocalAlbum(
137            DataManager manager, int type, Path parent, int id, String name) {
138        synchronized (DataManager.LOCK) {
139            Path path = parent.getChild(id);
140            MediaObject object = manager.peekMediaObject(path);
141            if (object != null) return (MediaSet) object;
142            switch (type) {
143                case MEDIA_TYPE_IMAGE:
144                    return new LocalAlbum(path, mApplication, id, true, name);
145                case MEDIA_TYPE_VIDEO:
146                    return new LocalAlbum(path, mApplication, id, false, name);
147                case MEDIA_TYPE_ALL:
148                    Comparator<MediaItem> comp = DataManager.sDateTakenComparator;
149                    return new LocalMergeAlbum(path, comp, new MediaSet[] {
150                            getLocalAlbum(manager, MEDIA_TYPE_IMAGE, PATH_IMAGE, id, name),
151                            getLocalAlbum(manager, MEDIA_TYPE_VIDEO, PATH_VIDEO, id, name)}, id);
152            }
153            throw new IllegalArgumentException(String.valueOf(type));
154        }
155    }
156
157    @Override
158    public synchronized boolean isLoading() {
159        return mIsLoading;
160    }
161
162    @Override
163    // synchronized on this function for
164    //   1. Prevent calling reload() concurrently.
165    //   2. Prevent calling onFutureDone() and reload() concurrently
166    public synchronized long reload() {
167        if (mNotifier.isDirty()) {
168            if (mLoadTask != null) mLoadTask.cancel();
169            mIsLoading = true;
170            mLoadTask = mApplication.getThreadPool().submit(new AlbumsLoader(), this);
171        }
172        if (mLoadBuffer != null) {
173            mAlbums = mLoadBuffer;
174            mLoadBuffer = null;
175            for (MediaSet album : mAlbums) {
176                album.reload();
177            }
178            mDataVersion = nextVersionNumber();
179        }
180        return mDataVersion;
181    }
182
183    @Override
184    public synchronized void onFutureDone(Future<ArrayList<MediaSet>> future) {
185        if (mLoadTask != future) return; // ignore, wait for the latest task
186        mLoadBuffer = future.get();
187        mIsLoading = false;
188        if (mLoadBuffer == null) mLoadBuffer = new ArrayList<MediaSet>();
189        mHandler.post(new Runnable() {
190            @Override
191            public void run() {
192                notifyContentChanged();
193            }
194        });
195    }
196
197    // For debug only. Fake there is a ContentObserver.onChange() event.
198    void fakeChange() {
199        mNotifier.fakeChange();
200    }
201
202    // Circular shift the array range from a[i] to a[j] (inclusive). That is,
203    // a[i] -> a[i+1] -> a[i+2] -> ... -> a[j], and a[j] -> a[i]
204    private static <T> void circularShiftRight(T[] array, int i, int j) {
205        T temp = array[j];
206        for (int k = j; k > i; k--) {
207            array[k] = array[k - 1];
208        }
209        array[i] = temp;
210    }
211}
212