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 17 package com.android.gallery3d.data; 18 19 import android.net.Uri; 20 import android.os.Handler; 21 import android.provider.MediaStore.Images; 22 import android.provider.MediaStore.Video; 23 24 import com.android.gallery3d.R; 25 import com.android.gallery3d.app.GalleryApp; 26 import com.android.gallery3d.data.BucketHelper.BucketEntry; 27 import com.android.gallery3d.util.Future; 28 import com.android.gallery3d.util.FutureListener; 29 import com.android.gallery3d.util.MediaSetUtils; 30 import com.android.gallery3d.util.ThreadPool; 31 import com.android.gallery3d.util.ThreadPool.JobContext; 32 33 import java.util.ArrayList; 34 import 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" 38 public 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