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.database.ContentObserver; 20 import android.net.Uri; 21 import android.os.Handler; 22 23 import com.android.gallery3d.app.GalleryApp; 24 import com.android.gallery3d.app.StitchingChangeListener; 25 import com.android.gallery3d.common.ApiHelper; 26 import com.android.gallery3d.common.Utils; 27 import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback; 28 import com.android.gallery3d.data.MediaSet.ItemConsumer; 29 import com.android.gallery3d.data.MediaSource.PathId; 30 import com.android.gallery3d.picasasource.PicasaSource; 31 32 import java.util.ArrayList; 33 import java.util.Comparator; 34 import java.util.HashMap; 35 import java.util.LinkedHashMap; 36 import java.util.Map.Entry; 37 import java.util.WeakHashMap; 38 39 // DataManager manages all media sets and media items in the system. 40 // 41 // Each MediaSet and MediaItem has a unique 64 bits id. The most significant 42 // 32 bits represents its parent, and the least significant 32 bits represents 43 // the self id. For MediaSet the self id is is globally unique, but for 44 // MediaItem it's unique only relative to its parent. 45 // 46 // To make sure the id is the same when the MediaSet is re-created, a child key 47 // is provided to obtainSetId() to make sure the same self id will be used as 48 // when the parent and key are the same. A sequence of child keys is called a 49 // path. And it's used to identify a specific media set even if the process is 50 // killed and re-created, so child keys should be stable identifiers. 51 52 public class DataManager implements StitchingChangeListener { 53 public static final int INCLUDE_IMAGE = 1; 54 public static final int INCLUDE_VIDEO = 2; 55 public static final int INCLUDE_ALL = INCLUDE_IMAGE | INCLUDE_VIDEO; 56 public static final int INCLUDE_LOCAL_ONLY = 4; 57 public static final int INCLUDE_LOCAL_IMAGE_ONLY = 58 INCLUDE_LOCAL_ONLY | INCLUDE_IMAGE; 59 public static final int INCLUDE_LOCAL_VIDEO_ONLY = 60 INCLUDE_LOCAL_ONLY | INCLUDE_VIDEO; 61 public static final int INCLUDE_LOCAL_ALL_ONLY = 62 INCLUDE_LOCAL_ONLY | INCLUDE_IMAGE | INCLUDE_VIDEO; 63 64 // Any one who would like to access data should require this lock 65 // to prevent concurrency issue. 66 public static final Object LOCK = new Object(); 67 68 private static final String TAG = "DataManager"; 69 70 // This is the path for the media set seen by the user at top level. 71 private static final String TOP_SET_PATH = ApiHelper.HAS_MTP 72 ? "/combo/{/mtp,/local/all,/picasa/all}" 73 : "/combo/{/local/all,/picasa/all}"; 74 75 private static final String TOP_IMAGE_SET_PATH = ApiHelper.HAS_MTP 76 ? "/combo/{/mtp,/local/image,/picasa/image}" 77 : "/combo/{/local/image,/picasa/image}"; 78 79 private static final String TOP_VIDEO_SET_PATH = 80 "/combo/{/local/video,/picasa/video}"; 81 82 private static final String TOP_LOCAL_SET_PATH = "/local/all"; 83 84 private static final String TOP_LOCAL_IMAGE_SET_PATH = "/local/image"; 85 86 private static final String TOP_LOCAL_VIDEO_SET_PATH = "/local/video"; 87 88 public static final Comparator<MediaItem> sDateTakenComparator = 89 new DateTakenComparator(); 90 91 private static class DateTakenComparator implements Comparator<MediaItem> { 92 @Override 93 public int compare(MediaItem item1, MediaItem item2) { 94 return -Utils.compare(item1.getDateInMs(), item2.getDateInMs()); 95 } 96 } 97 98 private final Handler mDefaultMainHandler; 99 100 private GalleryApp mApplication; 101 private int mActiveCount = 0; 102 103 private HashMap<Uri, NotifyBroker> mNotifierMap = 104 new HashMap<Uri, NotifyBroker>(); 105 106 107 private HashMap<String, MediaSource> mSourceMap = 108 new LinkedHashMap<String, MediaSource>(); 109 110 public DataManager(GalleryApp application) { 111 mApplication = application; 112 mDefaultMainHandler = new Handler(application.getMainLooper()); 113 } 114 115 public synchronized void initializeSourceMap() { 116 if (!mSourceMap.isEmpty()) return; 117 118 // the order matters, the UriSource must come last 119 addSource(new LocalSource(mApplication)); 120 addSource(new PicasaSource(mApplication)); 121 if (ApiHelper.HAS_MTP) { 122 addSource(new MtpSource(mApplication)); 123 } 124 addSource(new ComboSource(mApplication)); 125 addSource(new ClusterSource(mApplication)); 126 addSource(new FilterSource(mApplication)); 127 addSource(new SecureSource(mApplication)); 128 addSource(new UriSource(mApplication)); 129 addSource(new SnailSource(mApplication)); 130 131 if (mActiveCount > 0) { 132 for (MediaSource source : mSourceMap.values()) { 133 source.resume(); 134 } 135 } 136 } 137 138 public String getTopSetPath(int typeBits) { 139 140 switch (typeBits) { 141 case INCLUDE_IMAGE: return TOP_IMAGE_SET_PATH; 142 case INCLUDE_VIDEO: return TOP_VIDEO_SET_PATH; 143 case INCLUDE_ALL: return TOP_SET_PATH; 144 case INCLUDE_LOCAL_IMAGE_ONLY: return TOP_LOCAL_IMAGE_SET_PATH; 145 case INCLUDE_LOCAL_VIDEO_ONLY: return TOP_LOCAL_VIDEO_SET_PATH; 146 case INCLUDE_LOCAL_ALL_ONLY: return TOP_LOCAL_SET_PATH; 147 default: throw new IllegalArgumentException(); 148 } 149 } 150 151 // open for debug 152 void addSource(MediaSource source) { 153 if (source == null) return; 154 mSourceMap.put(source.getPrefix(), source); 155 } 156 157 // A common usage of this method is: 158 // synchronized (DataManager.LOCK) { 159 // MediaObject object = peekMediaObject(path); 160 // if (object == null) { 161 // object = createMediaObject(...); 162 // } 163 // } 164 public MediaObject peekMediaObject(Path path) { 165 return path.getObject(); 166 } 167 168 public MediaObject getMediaObject(Path path) { 169 synchronized (LOCK) { 170 MediaObject obj = path.getObject(); 171 if (obj != null) return obj; 172 173 MediaSource source = mSourceMap.get(path.getPrefix()); 174 if (source == null) { 175 Log.w(TAG, "cannot find media source for path: " + path); 176 return null; 177 } 178 179 try { 180 MediaObject object = source.createMediaObject(path); 181 if (object == null) { 182 Log.w(TAG, "cannot create media object: " + path); 183 } 184 return object; 185 } catch (Throwable t) { 186 Log.w(TAG, "exception in creating media object: " + path, t); 187 return null; 188 } 189 } 190 } 191 192 public MediaObject getMediaObject(String s) { 193 return getMediaObject(Path.fromString(s)); 194 } 195 196 public MediaSet getMediaSet(Path path) { 197 return (MediaSet) getMediaObject(path); 198 } 199 200 public MediaSet getMediaSet(String s) { 201 return (MediaSet) getMediaObject(s); 202 } 203 204 public MediaSet[] getMediaSetsFromString(String segment) { 205 String[] seq = Path.splitSequence(segment); 206 int n = seq.length; 207 MediaSet[] sets = new MediaSet[n]; 208 for (int i = 0; i < n; i++) { 209 sets[i] = getMediaSet(seq[i]); 210 } 211 return sets; 212 } 213 214 // Maps a list of Paths to MediaItems, and invoke consumer.consume() 215 // for each MediaItem (may not be in the same order as the input list). 216 // An index number is also passed to consumer.consume() to identify 217 // the original position in the input list of the corresponding Path (plus 218 // startIndex). 219 public void mapMediaItems(ArrayList<Path> list, ItemConsumer consumer, 220 int startIndex) { 221 HashMap<String, ArrayList<PathId>> map = 222 new HashMap<String, ArrayList<PathId>>(); 223 224 // Group the path by the prefix. 225 int n = list.size(); 226 for (int i = 0; i < n; i++) { 227 Path path = list.get(i); 228 String prefix = path.getPrefix(); 229 ArrayList<PathId> group = map.get(prefix); 230 if (group == null) { 231 group = new ArrayList<PathId>(); 232 map.put(prefix, group); 233 } 234 group.add(new PathId(path, i + startIndex)); 235 } 236 237 // For each group, ask the corresponding media source to map it. 238 for (Entry<String, ArrayList<PathId>> entry : map.entrySet()) { 239 String prefix = entry.getKey(); 240 MediaSource source = mSourceMap.get(prefix); 241 source.mapMediaItems(entry.getValue(), consumer); 242 } 243 } 244 245 // The following methods forward the request to the proper object. 246 public int getSupportedOperations(Path path) { 247 return getMediaObject(path).getSupportedOperations(); 248 } 249 250 public void getPanoramaSupport(Path path, PanoramaSupportCallback callback) { 251 getMediaObject(path).getPanoramaSupport(callback); 252 } 253 254 public void delete(Path path) { 255 getMediaObject(path).delete(); 256 } 257 258 public void rotate(Path path, int degrees) { 259 getMediaObject(path).rotate(degrees); 260 } 261 262 public Uri getContentUri(Path path) { 263 return getMediaObject(path).getContentUri(); 264 } 265 266 public int getMediaType(Path path) { 267 return getMediaObject(path).getMediaType(); 268 } 269 270 public Path findPathByUri(Uri uri, String type) { 271 if (uri == null) return null; 272 for (MediaSource source : mSourceMap.values()) { 273 Path path = source.findPathByUri(uri, type); 274 if (path != null) return path; 275 } 276 return null; 277 } 278 279 public Path getDefaultSetOf(Path item) { 280 MediaSource source = mSourceMap.get(item.getPrefix()); 281 return source == null ? null : source.getDefaultSetOf(item); 282 } 283 284 // Returns number of bytes used by cached pictures currently downloaded. 285 public long getTotalUsedCacheSize() { 286 long sum = 0; 287 for (MediaSource source : mSourceMap.values()) { 288 sum += source.getTotalUsedCacheSize(); 289 } 290 return sum; 291 } 292 293 // Returns number of bytes used by cached pictures if all pending 294 // downloads and removals are completed. 295 public long getTotalTargetCacheSize() { 296 long sum = 0; 297 for (MediaSource source : mSourceMap.values()) { 298 sum += source.getTotalTargetCacheSize(); 299 } 300 return sum; 301 } 302 303 public void registerChangeNotifier(Uri uri, ChangeNotifier notifier) { 304 NotifyBroker broker = null; 305 synchronized (mNotifierMap) { 306 broker = mNotifierMap.get(uri); 307 if (broker == null) { 308 broker = new NotifyBroker(mDefaultMainHandler); 309 mApplication.getContentResolver() 310 .registerContentObserver(uri, true, broker); 311 mNotifierMap.put(uri, broker); 312 } 313 } 314 broker.registerNotifier(notifier); 315 } 316 317 public void resume() { 318 if (++mActiveCount == 1) { 319 for (MediaSource source : mSourceMap.values()) { 320 source.resume(); 321 } 322 } 323 } 324 325 public void pause() { 326 if (--mActiveCount == 0) { 327 for (MediaSource source : mSourceMap.values()) { 328 source.pause(); 329 } 330 } 331 } 332 333 private static class NotifyBroker extends ContentObserver { 334 private WeakHashMap<ChangeNotifier, Object> mNotifiers = 335 new WeakHashMap<ChangeNotifier, Object>(); 336 337 public NotifyBroker(Handler handler) { 338 super(handler); 339 } 340 341 public synchronized void registerNotifier(ChangeNotifier notifier) { 342 mNotifiers.put(notifier, null); 343 } 344 345 @Override 346 public synchronized void onChange(boolean selfChange) { 347 for(ChangeNotifier notifier : mNotifiers.keySet()) { 348 notifier.onChange(selfChange); 349 } 350 } 351 } 352 353 @Override 354 public void onStitchingQueued(Uri uri) { 355 // Do nothing. 356 } 357 358 @Override 359 public void onStitchingResult(Uri uri) { 360 Path path = findPathByUri(uri, null); 361 if (path != null) { 362 MediaObject mediaObject = getMediaObject(path); 363 if (mediaObject != null) { 364 mediaObject.clearCachedPanoramaSupport(); 365 } 366 } 367 } 368 369 @Override 370 public void onStitchingProgress(Uri uri, int progress) { 371 // Do nothing. 372 } 373 } 374