From a2fba687d4d2dbb3b2db8866b054ecb0e42871b2 Mon Sep 17 00:00:00 2001 From: Owen Lin Date: Wed, 17 Aug 2011 22:07:43 +0800 Subject: Initial code for Gallery2. fix: 5176434 Change-Id: I041e282b9c7b34ceb1db8b033be2b853bb3a992c --- src/com/android/gallery3d/anim/AlphaAnimation.java | 48 + src/com/android/gallery3d/anim/Animation.java | 92 ++ src/com/android/gallery3d/anim/AnimationSet.java | 76 ++ .../android/gallery3d/anim/CanvasAnimation.java | 25 + src/com/android/gallery3d/anim/FloatAnimation.java | 40 + .../gallery3d/app/AbstractGalleryActivity.java | 198 ++++ src/com/android/gallery3d/app/ActivityState.java | 136 +++ .../android/gallery3d/app/AlbumDataAdapter.java | 367 ++++++ src/com/android/gallery3d/app/AlbumPage.java | 602 ++++++++++ src/com/android/gallery3d/app/AlbumPicker.java | 68 ++ .../android/gallery3d/app/AlbumSetDataAdapter.java | 384 +++++++ src/com/android/gallery3d/app/AlbumSetPage.java | 586 ++++++++++ src/com/android/gallery3d/app/Config.java | 140 +++ src/com/android/gallery3d/app/CropImage.java | 850 ++++++++++++++ src/com/android/gallery3d/app/DialogPicker.java | 68 ++ src/com/android/gallery3d/app/EyePosition.java | 218 ++++ src/com/android/gallery3d/app/FilterUtils.java | 296 +++++ src/com/android/gallery3d/app/Gallery.java | 232 ++++ .../android/gallery3d/app/GalleryActionBar.java | 218 ++++ src/com/android/gallery3d/app/GalleryActivity.java | 28 + src/com/android/gallery3d/app/GalleryApp.java | 39 + src/com/android/gallery3d/app/GalleryAppImpl.java | 90 ++ src/com/android/gallery3d/app/GalleryContext.java | 38 + src/com/android/gallery3d/app/LoadingListener.java | 22 + src/com/android/gallery3d/app/Log.java | 53 + src/com/android/gallery3d/app/ManageCachePage.java | 271 +++++ src/com/android/gallery3d/app/MovieActivity.java | 129 +++ src/com/android/gallery3d/app/MoviePlayer.java | 291 +++++ src/com/android/gallery3d/app/PackagesMonitor.java | 50 + .../android/gallery3d/app/PhotoDataAdapter.java | 794 +++++++++++++ src/com/android/gallery3d/app/PhotoPage.java | 581 ++++++++++ .../gallery3d/app/SinglePhotoDataAdapter.java | 181 +++ .../gallery3d/app/SlideshowDataAdapter.java | 187 +++ src/com/android/gallery3d/app/SlideshowDream.java | 28 + src/com/android/gallery3d/app/SlideshowPage.java | 338 ++++++ src/com/android/gallery3d/app/StateManager.java | 277 +++++ .../android/gallery3d/app/UsbDeviceActivity.java | 47 + src/com/android/gallery3d/app/Wallpaper.java | 116 ++ src/com/android/gallery3d/data/ChangeNotifier.java | 54 + src/com/android/gallery3d/data/ClusterAlbum.java | 129 +++ .../android/gallery3d/data/ClusterAlbumSet.java | 152 +++ src/com/android/gallery3d/data/ClusterSource.java | 86 ++ src/com/android/gallery3d/data/Clustering.java | 26 + src/com/android/gallery3d/data/ComboAlbum.java | 87 ++ src/com/android/gallery3d/data/ComboAlbumSet.java | 80 ++ src/com/android/gallery3d/data/ComboSource.java | 55 + .../android/gallery3d/data/ContentListener.java | 21 + src/com/android/gallery3d/data/DataManager.java | 333 ++++++ src/com/android/gallery3d/data/DecodeUtils.java | 173 +++ src/com/android/gallery3d/data/DownloadCache.java | 398 +++++++ src/com/android/gallery3d/data/DownloadEntry.java | 72 ++ src/com/android/gallery3d/data/DownloadUtils.java | 95 ++ src/com/android/gallery3d/data/Face.java | 56 + src/com/android/gallery3d/data/FaceClustering.java | 94 ++ src/com/android/gallery3d/data/FilterSet.java | 137 +++ src/com/android/gallery3d/data/FilterSource.java | 52 + .../android/gallery3d/data/ImageCacheRequest.java | 89 ++ .../android/gallery3d/data/ImageCacheService.java | 105 ++ src/com/android/gallery3d/data/LocalAlbum.java | 252 +++++ src/com/android/gallery3d/data/LocalAlbumSet.java | 263 +++++ src/com/android/gallery3d/data/LocalImage.java | 289 +++++ src/com/android/gallery3d/data/LocalMediaItem.java | 103 ++ .../android/gallery3d/data/LocalMergeAlbum.java | 226 ++++ src/com/android/gallery3d/data/LocalSource.java | 272 +++++ src/com/android/gallery3d/data/LocalVideo.java | 213 ++++ .../android/gallery3d/data/LocationClustering.java | 304 +++++ src/com/android/gallery3d/data/Log.java | 53 + src/com/android/gallery3d/data/MediaDetails.java | 162 +++ src/com/android/gallery3d/data/MediaItem.java | 75 ++ src/com/android/gallery3d/data/MediaObject.java | 130 +++ src/com/android/gallery3d/data/MediaSet.java | 219 ++++ src/com/android/gallery3d/data/MediaSource.java | 93 ++ src/com/android/gallery3d/data/MtpClient.java | 442 ++++++++ src/com/android/gallery3d/data/MtpContext.java | 141 +++ src/com/android/gallery3d/data/MtpDevice.java | 174 +++ src/com/android/gallery3d/data/MtpDeviceSet.java | 109 ++ src/com/android/gallery3d/data/MtpImage.java | 166 +++ src/com/android/gallery3d/data/MtpSource.java | 71 ++ src/com/android/gallery3d/data/Path.java | 237 ++++ src/com/android/gallery3d/data/PathMatcher.java | 102 ++ src/com/android/gallery3d/data/SizeClustering.java | 138 +++ src/com/android/gallery3d/data/TagClustering.java | 94 ++ src/com/android/gallery3d/data/TimeClustering.java | 436 +++++++ src/com/android/gallery3d/data/UriImage.java | 266 +++++ src/com/android/gallery3d/data/UriSource.java | 58 + .../gallery3d/provider/GalleryProvider.java | 224 ++++ .../android/gallery3d/ui/AbstractDisplayItem.java | 114 ++ .../android/gallery3d/ui/ActionModeHandler.java | 246 ++++ .../android/gallery3d/ui/AdaptiveBackground.java | 128 +++ .../gallery3d/ui/AlbumSetSlidingWindow.java | 543 +++++++++ src/com/android/gallery3d/ui/AlbumSetView.java | 240 ++++ .../android/gallery3d/ui/AlbumSlidingWindow.java | 433 +++++++ src/com/android/gallery3d/ui/AlbumView.java | 197 ++++ src/com/android/gallery3d/ui/BasicTexture.java | 164 +++ src/com/android/gallery3d/ui/BitmapTexture.java | 49 + .../android/gallery3d/ui/BitmapTileProvider.java | 91 ++ src/com/android/gallery3d/ui/BoxBlurFilter.java | 100 ++ src/com/android/gallery3d/ui/CacheBarView.java | 270 +++++ src/com/android/gallery3d/ui/CanvasTexture.java | 52 + src/com/android/gallery3d/ui/ColorTexture.java | 58 + src/com/android/gallery3d/ui/Config.java | 31 + src/com/android/gallery3d/ui/CropView.java | 801 +++++++++++++ src/com/android/gallery3d/ui/CustomMenu.java | 126 +++ src/com/android/gallery3d/ui/DetailsWindow.java | 451 ++++++++ src/com/android/gallery3d/ui/DisplayItem.java | 45 + src/com/android/gallery3d/ui/DownUpDetector.java | 61 + src/com/android/gallery3d/ui/DrawableTexture.java | 38 + src/com/android/gallery3d/ui/FilmStripView.java | 261 +++++ src/com/android/gallery3d/ui/GLCanvas.java | 138 +++ src/com/android/gallery3d/ui/GLCanvasImpl.java | 913 +++++++++++++++ src/com/android/gallery3d/ui/GLPaint.java | 65 ++ src/com/android/gallery3d/ui/GLRoot.java | 37 + src/com/android/gallery3d/ui/GLRootView.java | 414 +++++++ src/com/android/gallery3d/ui/GLView.java | 431 +++++++ .../gallery3d/ui/GalleryEGLConfigChooser.java | 126 +++ src/com/android/gallery3d/ui/GridDrawer.java | 109 ++ src/com/android/gallery3d/ui/HighlightDrawer.java | 73 ++ src/com/android/gallery3d/ui/Icon.java | 59 + src/com/android/gallery3d/ui/IconDrawer.java | 112 ++ .../gallery3d/ui/ImportCompleteListener.java | 57 + src/com/android/gallery3d/ui/Label.java | 84 ++ src/com/android/gallery3d/ui/Log.java | 53 + .../android/gallery3d/ui/ManageCacheDrawer.java | 126 +++ src/com/android/gallery3d/ui/MeasureHelper.java | 65 ++ src/com/android/gallery3d/ui/MenuExecutor.java | 398 +++++++ src/com/android/gallery3d/ui/MultiLineTexture.java | 50 + src/com/android/gallery3d/ui/NinePatchChunk.java | 82 ++ src/com/android/gallery3d/ui/NinePatchTexture.java | 401 +++++++ .../android/gallery3d/ui/OnSelectedListener.java | 21 + src/com/android/gallery3d/ui/Paper.java | 112 ++ src/com/android/gallery3d/ui/PhotoView.java | 1191 ++++++++++++++++++++ src/com/android/gallery3d/ui/PositionProvider.java | 23 + .../android/gallery3d/ui/PositionRepository.java | 139 +++ src/com/android/gallery3d/ui/ProgressBar.java | 65 ++ src/com/android/gallery3d/ui/ProgressSpinner.java | 78 ++ src/com/android/gallery3d/ui/RawTexture.java | 54 + src/com/android/gallery3d/ui/ResourceTexture.java | 52 + src/com/android/gallery3d/ui/ScrollBarView.java | 135 +++ src/com/android/gallery3d/ui/ScrollView.java | 99 ++ src/com/android/gallery3d/ui/ScrollerHelper.java | 93 ++ src/com/android/gallery3d/ui/SelectionDrawer.java | 89 ++ src/com/android/gallery3d/ui/SelectionManager.java | 221 ++++ src/com/android/gallery3d/ui/SlideshowView.java | 165 +++ src/com/android/gallery3d/ui/SlotView.java | 607 ++++++++++ src/com/android/gallery3d/ui/StaticBackground.java | 62 + src/com/android/gallery3d/ui/StringTexture.java | 92 ++ src/com/android/gallery3d/ui/StripDrawer.java | 57 + .../android/gallery3d/ui/SynchronizedHandler.java | 41 + src/com/android/gallery3d/ui/TextButton.java | 91 ++ src/com/android/gallery3d/ui/Texture.java | 44 + src/com/android/gallery3d/ui/TileImageView.java | 693 ++++++++++++ .../android/gallery3d/ui/TileImageViewAdapter.java | 144 +++ src/com/android/gallery3d/ui/UploadedTexture.java | 285 +++++ .../gallery3d/ui/UserInteractionListener.java | 26 + src/com/android/gallery3d/util/CacheManager.java | 82 ++ src/com/android/gallery3d/util/Future.java | 35 + src/com/android/gallery3d/util/FutureListener.java | 21 + src/com/android/gallery3d/util/FutureTask.java | 86 ++ src/com/android/gallery3d/util/GalleryUtils.java | 327 ++++++ src/com/android/gallery3d/util/IdentityCache.java | 74 ++ src/com/android/gallery3d/util/IntArray.java | 54 + .../gallery3d/util/InterruptableOutputStream.java | 67 ++ src/com/android/gallery3d/util/LinkedNode.java | 75 ++ src/com/android/gallery3d/util/Log.java | 53 + src/com/android/gallery3d/util/MediaSetUtils.java | 56 + .../gallery3d/util/PriorityThreadFactory.java | 48 + .../android/gallery3d/util/ReverseGeocoder.java | 417 +++++++ src/com/android/gallery3d/util/ThreadPool.java | 252 +++++ src/com/android/gallery3d/util/UpdateHelper.java | 67 ++ .../android/gallery3d/widget/LocalPhotoSource.java | 202 ++++ .../android/gallery3d/widget/MediaSetSource.java | 113 ++ .../gallery3d/widget/WidgetClickHandler.java | 59 + .../android/gallery3d/widget/WidgetConfigure.java | 167 +++ .../gallery3d/widget/WidgetDatabaseHelper.java | 187 +++ .../android/gallery3d/widget/WidgetProvider.java | 109 ++ .../android/gallery3d/widget/WidgetService.java | 169 +++ src/com/android/gallery3d/widget/WidgetSource.java | 31 + .../gallery3d/widget/WidgetTypeChooser.java | 59 + src/com/android/gallery3d/widget/WidgetUtils.java | 80 ++ 179 files changed, 31671 insertions(+) create mode 100644 src/com/android/gallery3d/anim/AlphaAnimation.java create mode 100644 src/com/android/gallery3d/anim/Animation.java create mode 100644 src/com/android/gallery3d/anim/AnimationSet.java create mode 100644 src/com/android/gallery3d/anim/CanvasAnimation.java create mode 100644 src/com/android/gallery3d/anim/FloatAnimation.java create mode 100644 src/com/android/gallery3d/app/AbstractGalleryActivity.java create mode 100644 src/com/android/gallery3d/app/ActivityState.java create mode 100644 src/com/android/gallery3d/app/AlbumDataAdapter.java create mode 100644 src/com/android/gallery3d/app/AlbumPage.java create mode 100644 src/com/android/gallery3d/app/AlbumPicker.java create mode 100644 src/com/android/gallery3d/app/AlbumSetDataAdapter.java create mode 100644 src/com/android/gallery3d/app/AlbumSetPage.java create mode 100644 src/com/android/gallery3d/app/Config.java create mode 100644 src/com/android/gallery3d/app/CropImage.java create mode 100644 src/com/android/gallery3d/app/DialogPicker.java create mode 100644 src/com/android/gallery3d/app/EyePosition.java create mode 100644 src/com/android/gallery3d/app/FilterUtils.java create mode 100644 src/com/android/gallery3d/app/Gallery.java create mode 100644 src/com/android/gallery3d/app/GalleryActionBar.java create mode 100644 src/com/android/gallery3d/app/GalleryActivity.java create mode 100644 src/com/android/gallery3d/app/GalleryApp.java create mode 100644 src/com/android/gallery3d/app/GalleryAppImpl.java create mode 100644 src/com/android/gallery3d/app/GalleryContext.java create mode 100644 src/com/android/gallery3d/app/LoadingListener.java create mode 100644 src/com/android/gallery3d/app/Log.java create mode 100644 src/com/android/gallery3d/app/ManageCachePage.java create mode 100644 src/com/android/gallery3d/app/MovieActivity.java create mode 100644 src/com/android/gallery3d/app/MoviePlayer.java create mode 100644 src/com/android/gallery3d/app/PackagesMonitor.java create mode 100644 src/com/android/gallery3d/app/PhotoDataAdapter.java create mode 100644 src/com/android/gallery3d/app/PhotoPage.java create mode 100644 src/com/android/gallery3d/app/SinglePhotoDataAdapter.java create mode 100644 src/com/android/gallery3d/app/SlideshowDataAdapter.java create mode 100644 src/com/android/gallery3d/app/SlideshowDream.java create mode 100644 src/com/android/gallery3d/app/SlideshowPage.java create mode 100644 src/com/android/gallery3d/app/StateManager.java create mode 100644 src/com/android/gallery3d/app/UsbDeviceActivity.java create mode 100644 src/com/android/gallery3d/app/Wallpaper.java create mode 100644 src/com/android/gallery3d/data/ChangeNotifier.java create mode 100644 src/com/android/gallery3d/data/ClusterAlbum.java create mode 100644 src/com/android/gallery3d/data/ClusterAlbumSet.java create mode 100644 src/com/android/gallery3d/data/ClusterSource.java create mode 100644 src/com/android/gallery3d/data/Clustering.java create mode 100644 src/com/android/gallery3d/data/ComboAlbum.java create mode 100644 src/com/android/gallery3d/data/ComboAlbumSet.java create mode 100644 src/com/android/gallery3d/data/ComboSource.java create mode 100644 src/com/android/gallery3d/data/ContentListener.java create mode 100644 src/com/android/gallery3d/data/DataManager.java create mode 100644 src/com/android/gallery3d/data/DecodeUtils.java create mode 100644 src/com/android/gallery3d/data/DownloadCache.java create mode 100644 src/com/android/gallery3d/data/DownloadEntry.java create mode 100644 src/com/android/gallery3d/data/DownloadUtils.java create mode 100644 src/com/android/gallery3d/data/Face.java create mode 100644 src/com/android/gallery3d/data/FaceClustering.java create mode 100644 src/com/android/gallery3d/data/FilterSet.java create mode 100644 src/com/android/gallery3d/data/FilterSource.java create mode 100644 src/com/android/gallery3d/data/ImageCacheRequest.java create mode 100644 src/com/android/gallery3d/data/ImageCacheService.java create mode 100644 src/com/android/gallery3d/data/LocalAlbum.java create mode 100644 src/com/android/gallery3d/data/LocalAlbumSet.java create mode 100644 src/com/android/gallery3d/data/LocalImage.java create mode 100644 src/com/android/gallery3d/data/LocalMediaItem.java create mode 100644 src/com/android/gallery3d/data/LocalMergeAlbum.java create mode 100644 src/com/android/gallery3d/data/LocalSource.java create mode 100644 src/com/android/gallery3d/data/LocalVideo.java create mode 100644 src/com/android/gallery3d/data/LocationClustering.java create mode 100644 src/com/android/gallery3d/data/Log.java create mode 100644 src/com/android/gallery3d/data/MediaDetails.java create mode 100644 src/com/android/gallery3d/data/MediaItem.java create mode 100644 src/com/android/gallery3d/data/MediaObject.java create mode 100644 src/com/android/gallery3d/data/MediaSet.java create mode 100644 src/com/android/gallery3d/data/MediaSource.java create mode 100644 src/com/android/gallery3d/data/MtpClient.java create mode 100644 src/com/android/gallery3d/data/MtpContext.java create mode 100644 src/com/android/gallery3d/data/MtpDevice.java create mode 100644 src/com/android/gallery3d/data/MtpDeviceSet.java create mode 100644 src/com/android/gallery3d/data/MtpImage.java create mode 100644 src/com/android/gallery3d/data/MtpSource.java create mode 100644 src/com/android/gallery3d/data/Path.java create mode 100644 src/com/android/gallery3d/data/PathMatcher.java create mode 100644 src/com/android/gallery3d/data/SizeClustering.java create mode 100644 src/com/android/gallery3d/data/TagClustering.java create mode 100644 src/com/android/gallery3d/data/TimeClustering.java create mode 100644 src/com/android/gallery3d/data/UriImage.java create mode 100644 src/com/android/gallery3d/data/UriSource.java create mode 100644 src/com/android/gallery3d/provider/GalleryProvider.java create mode 100644 src/com/android/gallery3d/ui/AbstractDisplayItem.java create mode 100644 src/com/android/gallery3d/ui/ActionModeHandler.java create mode 100644 src/com/android/gallery3d/ui/AdaptiveBackground.java create mode 100644 src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java create mode 100644 src/com/android/gallery3d/ui/AlbumSetView.java create mode 100644 src/com/android/gallery3d/ui/AlbumSlidingWindow.java create mode 100644 src/com/android/gallery3d/ui/AlbumView.java create mode 100644 src/com/android/gallery3d/ui/BasicTexture.java create mode 100644 src/com/android/gallery3d/ui/BitmapTexture.java create mode 100644 src/com/android/gallery3d/ui/BitmapTileProvider.java create mode 100644 src/com/android/gallery3d/ui/BoxBlurFilter.java create mode 100644 src/com/android/gallery3d/ui/CacheBarView.java create mode 100644 src/com/android/gallery3d/ui/CanvasTexture.java create mode 100644 src/com/android/gallery3d/ui/ColorTexture.java create mode 100644 src/com/android/gallery3d/ui/Config.java create mode 100644 src/com/android/gallery3d/ui/CropView.java create mode 100644 src/com/android/gallery3d/ui/CustomMenu.java create mode 100644 src/com/android/gallery3d/ui/DetailsWindow.java create mode 100644 src/com/android/gallery3d/ui/DisplayItem.java create mode 100644 src/com/android/gallery3d/ui/DownUpDetector.java create mode 100644 src/com/android/gallery3d/ui/DrawableTexture.java create mode 100644 src/com/android/gallery3d/ui/FilmStripView.java create mode 100644 src/com/android/gallery3d/ui/GLCanvas.java create mode 100644 src/com/android/gallery3d/ui/GLCanvasImpl.java create mode 100644 src/com/android/gallery3d/ui/GLPaint.java create mode 100644 src/com/android/gallery3d/ui/GLRoot.java create mode 100644 src/com/android/gallery3d/ui/GLRootView.java create mode 100644 src/com/android/gallery3d/ui/GLView.java create mode 100644 src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java create mode 100644 src/com/android/gallery3d/ui/GridDrawer.java create mode 100644 src/com/android/gallery3d/ui/HighlightDrawer.java create mode 100644 src/com/android/gallery3d/ui/Icon.java create mode 100644 src/com/android/gallery3d/ui/IconDrawer.java create mode 100644 src/com/android/gallery3d/ui/ImportCompleteListener.java create mode 100644 src/com/android/gallery3d/ui/Label.java create mode 100644 src/com/android/gallery3d/ui/Log.java create mode 100644 src/com/android/gallery3d/ui/ManageCacheDrawer.java create mode 100644 src/com/android/gallery3d/ui/MeasureHelper.java create mode 100644 src/com/android/gallery3d/ui/MenuExecutor.java create mode 100644 src/com/android/gallery3d/ui/MultiLineTexture.java create mode 100644 src/com/android/gallery3d/ui/NinePatchChunk.java create mode 100644 src/com/android/gallery3d/ui/NinePatchTexture.java create mode 100644 src/com/android/gallery3d/ui/OnSelectedListener.java create mode 100644 src/com/android/gallery3d/ui/Paper.java create mode 100644 src/com/android/gallery3d/ui/PhotoView.java create mode 100644 src/com/android/gallery3d/ui/PositionProvider.java create mode 100644 src/com/android/gallery3d/ui/PositionRepository.java create mode 100644 src/com/android/gallery3d/ui/ProgressBar.java create mode 100644 src/com/android/gallery3d/ui/ProgressSpinner.java create mode 100644 src/com/android/gallery3d/ui/RawTexture.java create mode 100644 src/com/android/gallery3d/ui/ResourceTexture.java create mode 100644 src/com/android/gallery3d/ui/ScrollBarView.java create mode 100644 src/com/android/gallery3d/ui/ScrollView.java create mode 100644 src/com/android/gallery3d/ui/ScrollerHelper.java create mode 100644 src/com/android/gallery3d/ui/SelectionDrawer.java create mode 100644 src/com/android/gallery3d/ui/SelectionManager.java create mode 100644 src/com/android/gallery3d/ui/SlideshowView.java create mode 100644 src/com/android/gallery3d/ui/SlotView.java create mode 100644 src/com/android/gallery3d/ui/StaticBackground.java create mode 100644 src/com/android/gallery3d/ui/StringTexture.java create mode 100644 src/com/android/gallery3d/ui/StripDrawer.java create mode 100644 src/com/android/gallery3d/ui/SynchronizedHandler.java create mode 100644 src/com/android/gallery3d/ui/TextButton.java create mode 100644 src/com/android/gallery3d/ui/Texture.java create mode 100644 src/com/android/gallery3d/ui/TileImageView.java create mode 100644 src/com/android/gallery3d/ui/TileImageViewAdapter.java create mode 100644 src/com/android/gallery3d/ui/UploadedTexture.java create mode 100644 src/com/android/gallery3d/ui/UserInteractionListener.java create mode 100644 src/com/android/gallery3d/util/CacheManager.java create mode 100644 src/com/android/gallery3d/util/Future.java create mode 100644 src/com/android/gallery3d/util/FutureListener.java create mode 100644 src/com/android/gallery3d/util/FutureTask.java create mode 100644 src/com/android/gallery3d/util/GalleryUtils.java create mode 100644 src/com/android/gallery3d/util/IdentityCache.java create mode 100644 src/com/android/gallery3d/util/IntArray.java create mode 100644 src/com/android/gallery3d/util/InterruptableOutputStream.java create mode 100644 src/com/android/gallery3d/util/LinkedNode.java create mode 100644 src/com/android/gallery3d/util/Log.java create mode 100644 src/com/android/gallery3d/util/MediaSetUtils.java create mode 100644 src/com/android/gallery3d/util/PriorityThreadFactory.java create mode 100644 src/com/android/gallery3d/util/ReverseGeocoder.java create mode 100644 src/com/android/gallery3d/util/ThreadPool.java create mode 100644 src/com/android/gallery3d/util/UpdateHelper.java create mode 100644 src/com/android/gallery3d/widget/LocalPhotoSource.java create mode 100644 src/com/android/gallery3d/widget/MediaSetSource.java create mode 100644 src/com/android/gallery3d/widget/WidgetClickHandler.java create mode 100644 src/com/android/gallery3d/widget/WidgetConfigure.java create mode 100644 src/com/android/gallery3d/widget/WidgetDatabaseHelper.java create mode 100644 src/com/android/gallery3d/widget/WidgetProvider.java create mode 100644 src/com/android/gallery3d/widget/WidgetService.java create mode 100644 src/com/android/gallery3d/widget/WidgetSource.java create mode 100644 src/com/android/gallery3d/widget/WidgetTypeChooser.java create mode 100644 src/com/android/gallery3d/widget/WidgetUtils.java (limited to 'src') diff --git a/src/com/android/gallery3d/anim/AlphaAnimation.java b/src/com/android/gallery3d/anim/AlphaAnimation.java new file mode 100644 index 000000000..cb17527b8 --- /dev/null +++ b/src/com/android/gallery3d/anim/AlphaAnimation.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.anim; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.ui.GLCanvas; + +public class AlphaAnimation extends CanvasAnimation { + private final float mStartAlpha; + private final float mEndAlpha; + private float mCurrentAlpha; + + public AlphaAnimation(float from, float to) { + mStartAlpha = from; + mEndAlpha = to; + mCurrentAlpha = from; + } + + @Override + public void apply(GLCanvas canvas) { + canvas.multiplyAlpha(mCurrentAlpha); + } + + @Override + public int getCanvasSaveFlags() { + return GLCanvas.SAVE_FLAG_ALPHA; + } + + @Override + protected void onCalculate(float progress) { + mCurrentAlpha = Utils.clamp(mStartAlpha + + (mEndAlpha - mStartAlpha) * progress, 0f, 1f); + } +} diff --git a/src/com/android/gallery3d/anim/Animation.java b/src/com/android/gallery3d/anim/Animation.java new file mode 100644 index 000000000..bd5a6cd72 --- /dev/null +++ b/src/com/android/gallery3d/anim/Animation.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.anim; + +import com.android.gallery3d.common.Utils; + +import android.view.animation.Interpolator; + +// Animation calculates a value according to the current input time. +// +// 1. First we need to use setDuration(int) to set the duration of the +// animation. The duration is in milliseconds. +// 2. Then we should call start(). The actual start time is the first value +// passed to calculate(long). +// 3. Each time we want to get an animation value, we call +// calculate(long currentTimeMillis) to ask the Animation to calculate it. +// The parameter passed to calculate(long) should be nonnegative. +// 4. Use get() to get that value. +// +// In step 3, onCalculate(float progress) is called so subclasses can calculate +// the value according to progress (progress is a value in [0,1]). +// +// Before onCalculate(float) is called, There is an optional interpolator which +// can change the progress value. The interpolator can be set by +// setInterpolator(Interpolator). If the interpolator is used, the value passed +// to onCalculate may be (for example, the overshoot effect). +// +// The isActive() method returns true after the animation start() is called and +// before calculate is passed a value which reaches the duration of the +// animation. +// +// The start() method can be called again to restart the Animation. +// +abstract public class Animation { + private static final long ANIMATION_START = -1; + private static final long NO_ANIMATION = -2; + + private long mStartTime = NO_ANIMATION; + private int mDuration; + private Interpolator mInterpolator; + + public void setInterpolator(Interpolator interpolator) { + mInterpolator = interpolator; + } + + public void setDuration(int duration) { + mDuration = duration; + } + + public void start() { + mStartTime = ANIMATION_START; + } + + public void setStartTime(long time) { + mStartTime = time; + } + + public boolean isActive() { + return mStartTime != NO_ANIMATION; + } + + public void forceStop() { + mStartTime = NO_ANIMATION; + } + + public boolean calculate(long currentTimeMillis) { + if (mStartTime == NO_ANIMATION) return false; + if (mStartTime == ANIMATION_START) mStartTime = currentTimeMillis; + int elapse = (int) (currentTimeMillis - mStartTime); + float x = Utils.clamp((float) elapse / mDuration, 0f, 1f); + Interpolator i = mInterpolator; + onCalculate(i != null ? i.getInterpolation(x) : x); + if (elapse >= mDuration) mStartTime = NO_ANIMATION; + return mStartTime != NO_ANIMATION; + } + + abstract protected void onCalculate(float progress); +} diff --git a/src/com/android/gallery3d/anim/AnimationSet.java b/src/com/android/gallery3d/anim/AnimationSet.java new file mode 100644 index 000000000..773cb4314 --- /dev/null +++ b/src/com/android/gallery3d/anim/AnimationSet.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.anim; + +import com.android.gallery3d.ui.GLCanvas; + +import java.util.ArrayList; + +public class AnimationSet extends CanvasAnimation { + + private final ArrayList mAnimations = + new ArrayList(); + private int mSaveFlags = 0; + + + public void addAnimation(CanvasAnimation anim) { + mAnimations.add(anim); + mSaveFlags |= anim.getCanvasSaveFlags(); + } + + @Override + public void apply(GLCanvas canvas) { + for (int i = 0, n = mAnimations.size(); i < n; i++) { + mAnimations.get(i).apply(canvas); + } + } + + @Override + public int getCanvasSaveFlags() { + return mSaveFlags; + } + + @Override + protected void onCalculate(float progress) { + // DO NOTHING + } + + @Override + public boolean calculate(long currentTimeMillis) { + boolean more = false; + for (CanvasAnimation anim : mAnimations) { + more |= anim.calculate(currentTimeMillis); + } + return more; + } + + @Override + public void start() { + for (CanvasAnimation anim : mAnimations) { + anim.start(); + } + } + + @Override + public boolean isActive() { + for (CanvasAnimation anim : mAnimations) { + if (anim.isActive()) return true; + } + return false; + } + +} diff --git a/src/com/android/gallery3d/anim/CanvasAnimation.java b/src/com/android/gallery3d/anim/CanvasAnimation.java new file mode 100644 index 000000000..4c8bcc825 --- /dev/null +++ b/src/com/android/gallery3d/anim/CanvasAnimation.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.anim; + +import com.android.gallery3d.ui.GLCanvas; + +public abstract class CanvasAnimation extends Animation { + + public abstract int getCanvasSaveFlags(); + public abstract void apply(GLCanvas canvas); +} diff --git a/src/com/android/gallery3d/anim/FloatAnimation.java b/src/com/android/gallery3d/anim/FloatAnimation.java new file mode 100644 index 000000000..1294ec2f4 --- /dev/null +++ b/src/com/android/gallery3d/anim/FloatAnimation.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.anim; + +public class FloatAnimation extends Animation { + + private final float mFrom; + private final float mTo; + private float mCurrent; + + public FloatAnimation(float from, float to, int duration) { + mFrom = from; + mTo = to; + mCurrent = from; + setDuration(duration); + } + + @Override + protected void onCalculate(float progress) { + mCurrent = mFrom + (mTo - mFrom) * progress; + } + + public float get() { + return mCurrent; + } +} diff --git a/src/com/android/gallery3d/app/AbstractGalleryActivity.java b/src/com/android/gallery3d/app/AbstractGalleryActivity.java new file mode 100644 index 000000000..d0d7b0fad --- /dev/null +++ b/src/com/android/gallery3d/app/AbstractGalleryActivity.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.ImageCacheService; +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.GLRootView; +import com.android.gallery3d.ui.PositionRepository; +import com.android.gallery3d.util.ThreadPool; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; + +public class AbstractGalleryActivity extends Activity implements GalleryActivity { + @SuppressWarnings("unused") + private static final String TAG = "AbstractGalleryActivity"; + private GLRootView mGLRootView; + private StateManager mStateManager; + private PositionRepository mPositionRepository = new PositionRepository(); + + private AlertDialog mAlertDialog = null; + private BroadcastReceiver mMountReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (getExternalCacheDir() != null) onStorageReady(); + } + }; + private IntentFilter mMountFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED); + + @Override + protected void onSaveInstanceState(Bundle outState) { + mGLRootView.lockRenderThread(); + try { + super.onSaveInstanceState(outState); + getStateManager().saveState(outState); + } finally { + mGLRootView.unlockRenderThread(); + } + } + + public Context getAndroidContext() { + return this; + } + + public ImageCacheService getImageCacheService() { + return ((GalleryApp) getApplication()).getImageCacheService(); + } + + public DataManager getDataManager() { + return ((GalleryApp) getApplication()).getDataManager(); + } + + public ThreadPool getThreadPool() { + return ((GalleryApp) getApplication()).getThreadPool(); + } + + public GalleryApp getGalleryApplication() { + return (GalleryApp) getApplication(); + } + + public synchronized StateManager getStateManager() { + if (mStateManager == null) { + mStateManager = new StateManager(this); + } + return mStateManager; + } + + public GLRoot getGLRoot() { + return mGLRootView; + } + + public PositionRepository getPositionRepository() { + return mPositionRepository; + } + + @Override + public void setContentView(int resId) { + super.setContentView(resId); + mGLRootView = (GLRootView) findViewById(R.id.gl_root_view); + } + + public int getActionBarHeight() { + ActionBar actionBar = getActionBar(); + return actionBar != null ? actionBar.getHeight() : 0; + } + + protected void onStorageReady() { + if (mAlertDialog != null) { + mAlertDialog.dismiss(); + mAlertDialog = null; + unregisterReceiver(mMountReceiver); + } + } + + @Override + protected void onStart() { + super.onStart(); + if (getExternalCacheDir() == null) { + OnCancelListener onCancel = new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + finish(); + } + }; + OnClickListener onClick = new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }; + mAlertDialog = new AlertDialog.Builder(this) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle("No Storage") + .setMessage("No external storage available.") + .setNegativeButton(android.R.string.cancel, onClick) + .setOnCancelListener(onCancel) + .show(); + registerReceiver(mMountReceiver, mMountFilter); + } + } + + @Override + protected void onStop() { + super.onStop(); + if (mAlertDialog != null) { + unregisterReceiver(mMountReceiver); + mAlertDialog.dismiss(); + mAlertDialog = null; + } + } + + @Override + protected void onResume() { + super.onResume(); + mGLRootView.lockRenderThread(); + try { + getStateManager().resume(); + getDataManager().resume(); + } finally { + mGLRootView.unlockRenderThread(); + } + mGLRootView.onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + mGLRootView.onPause(); + mGLRootView.lockRenderThread(); + try { + getStateManager().pause(); + getDataManager().pause(); + } finally { + mGLRootView.unlockRenderThread(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + mGLRootView.lockRenderThread(); + try { + getStateManager().notifyActivityResult( + requestCode, resultCode, data); + } finally { + mGLRootView.unlockRenderThread(); + } + } + + @Override + public GalleryActionBar getGalleryActionBar() { + return null; + } +} diff --git a/src/com/android/gallery3d/app/ActivityState.java b/src/com/android/gallery3d/app/ActivityState.java new file mode 100644 index 000000000..bfacc5484 --- /dev/null +++ b/src/com/android/gallery3d/app/ActivityState.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.ui.GLView; + +import android.app.ActionBar; +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; + +abstract public class ActivityState { + public static final int FLAG_HIDE_ACTION_BAR = 1; + public static final int FLAG_HIDE_STATUS_BAR = 2; + + protected GalleryActivity mActivity; + protected Bundle mData; + protected int mFlags; + + protected ResultEntry mReceivedResults; + protected ResultEntry mResult; + + protected static class ResultEntry { + public int requestCode; + public int resultCode = Activity.RESULT_CANCELED; + public Intent resultData; + ResultEntry next; + } + + protected ActivityState() { + } + + protected void setContentPane(GLView content) { + mActivity.getGLRoot().setContentPane(content); + } + + void initialize(GalleryActivity activity, Bundle data) { + mActivity = activity; + mData = data; + } + + public Bundle getData() { + return mData; + } + + protected void onBackPressed() { + mActivity.getStateManager().finishState(this); + } + + protected void setStateResult(int resultCode, Intent data) { + if (mResult == null) return; + mResult.resultCode = resultCode; + mResult.resultData = data; + } + + protected void onSaveState(Bundle outState) { + } + + protected void onStateResult(int requestCode, int resultCode, Intent data) { + } + + protected void onCreate(Bundle data, Bundle storedState) { + } + + protected void onPause() { + } + + // should only be called by StateManager + void resume() { + Activity activity = (Activity) mActivity; + ActionBar actionBar = activity.getActionBar(); + if (actionBar != null) { + if ((mFlags & FLAG_HIDE_ACTION_BAR) != 0) { + actionBar.hide(); + } else { + actionBar.show(); + } + int stateCount = mActivity.getStateManager().getStateCount(); + actionBar.setDisplayOptions( + stateCount == 1 ? 0 : ActionBar.DISPLAY_HOME_AS_UP, + ActionBar.DISPLAY_HOME_AS_UP); + } + + activity.invalidateOptionsMenu(); + + if ((mFlags & FLAG_HIDE_STATUS_BAR) != 0) { + WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); + params.systemUiVisibility = View.STATUS_BAR_HIDDEN; + ((Activity) mActivity).getWindow().setAttributes(params); + } else { + WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); + params.systemUiVisibility = View.STATUS_BAR_VISIBLE; + ((Activity) mActivity).getWindow().setAttributes(params); + } + + ResultEntry entry = mReceivedResults; + if (entry != null) { + mReceivedResults = null; + onStateResult(entry.requestCode, entry.resultCode, entry.resultData); + } + onResume(); + } + + // a subclass of ActivityState should override the method to resume itself + protected void onResume() { + } + + protected boolean onCreateActionBar(Menu menu) { + return false; + } + + protected boolean onItemSelected(MenuItem item) { + return false; + } + + protected void onDestroy() { + } +} diff --git a/src/com/android/gallery3d/app/AlbumDataAdapter.java b/src/com/android/gallery3d/app/AlbumDataAdapter.java new file mode 100644 index 000000000..9934cf88c --- /dev/null +++ b/src/com/android/gallery3d/app/AlbumDataAdapter.java @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.ui.AlbumView; +import com.android.gallery3d.ui.SynchronizedHandler; + +import android.os.Handler; +import android.os.Message; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +public class AlbumDataAdapter implements AlbumView.Model { + @SuppressWarnings("unused") + private static final String TAG = "AlbumDataAdapter"; + private static final int DATA_CACHE_SIZE = 1000; + + private static final int MSG_LOAD_START = 1; + private static final int MSG_LOAD_FINISH = 2; + private static final int MSG_RUN_OBJECT = 3; + + private static final int MIN_LOAD_COUNT = 32; + private static final int MAX_LOAD_COUNT = 64; + + private final MediaItem[] mData; + private final long[] mItemVersion; + private final long[] mSetVersion; + + private int mActiveStart = 0; + private int mActiveEnd = 0; + + private int mContentStart = 0; + private int mContentEnd = 0; + + private final MediaSet mSource; + private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; + + private final Handler mMainHandler; + private int mSize = 0; + + private AlbumView.ModelListener mModelListener; + private MySourceListener mSourceListener = new MySourceListener(); + private LoadingListener mLoadingListener; + + private ReloadTask mReloadTask; + + public AlbumDataAdapter(GalleryActivity context, MediaSet mediaSet) { + mSource = mediaSet; + + mData = new MediaItem[DATA_CACHE_SIZE]; + mItemVersion = new long[DATA_CACHE_SIZE]; + mSetVersion = new long[DATA_CACHE_SIZE]; + Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION); + Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION); + + mMainHandler = new SynchronizedHandler(context.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_RUN_OBJECT: + ((Runnable) message.obj).run(); + return; + case MSG_LOAD_START: + if (mLoadingListener != null) mLoadingListener.onLoadingStarted(); + return; + case MSG_LOAD_FINISH: + if (mLoadingListener != null) mLoadingListener.onLoadingFinished(); + return; + } + } + }; + } + + public void resume() { + mSource.addContentListener(mSourceListener); + mReloadTask = new ReloadTask(); + mReloadTask.start(); + } + + public void pause() { + mReloadTask.terminate(); + mReloadTask = null; + mSource.removeContentListener(mSourceListener); + } + + public MediaItem get(int index) { + if (!isActive(index)) { + throw new IllegalArgumentException(String.format( + "%s not in (%s, %s)", index, mActiveStart, mActiveEnd)); + } + return mData[index % mData.length]; + } + + public int getActiveStart() { + return mActiveStart; + } + + public int getActiveEnd() { + return mActiveEnd; + } + + public boolean isActive(int index) { + return index >= mActiveStart && index < mActiveEnd; + } + + public int size() { + return mSize; + } + + private void clearSlot(int slotIndex) { + mData[slotIndex] = null; + mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; + mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; + } + + private void setContentWindow(int contentStart, int contentEnd) { + if (contentStart == mContentStart && contentEnd == mContentEnd) return; + int end = mContentEnd; + int start = mContentStart; + + // We need change the content window before calling reloadData(...) + synchronized (this) { + mContentStart = contentStart; + mContentEnd = contentEnd; + } + MediaItem[] data = mData; + long[] itemVersion = mItemVersion; + long[] setVersion = mSetVersion; + if (contentStart >= end || start >= contentEnd) { + for (int i = start, n = end; i < n; ++i) { + clearSlot(i % DATA_CACHE_SIZE); + } + } else { + for (int i = start; i < contentStart; ++i) { + clearSlot(i % DATA_CACHE_SIZE); + } + for (int i = contentEnd, n = end; i < n; ++i) { + clearSlot(i % DATA_CACHE_SIZE); + } + } + if (mReloadTask != null) mReloadTask.notifyDirty(); + } + + public void setActiveWindow(int start, int end) { + if (start == mActiveStart && end == mActiveEnd) return; + + mActiveStart = start; + mActiveEnd = end; + + Utils.assertTrue(start <= end + && end - start <= mData.length && end <= mSize); + + int length = mData.length; + mActiveStart = start; + mActiveEnd = end; + + // If no data is visible, keep the cache content + if (start == end) return; + + int contentStart = Utils.clamp((start + end) / 2 - length / 2, + 0, Math.max(0, mSize - length)); + int contentEnd = Math.min(contentStart + length, mSize); + if (mContentStart > start || mContentEnd < end + || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) { + setContentWindow(contentStart, contentEnd); + } + } + + private class MySourceListener implements ContentListener { + public void onContentDirty() { + if (mReloadTask != null) mReloadTask.notifyDirty(); + } + } + + public void setModelListener(AlbumView.ModelListener listener) { + mModelListener = listener; + } + + public void setLoadingListener(LoadingListener listener) { + mLoadingListener = listener; + } + + private T executeAndWait(Callable callable) { + FutureTask task = new FutureTask(callable); + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); + try { + return task.get(); + } catch (InterruptedException e) { + return null; + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + private static class UpdateInfo { + public long version; + public int reloadStart; + public int reloadCount; + + public int size; + public ArrayList items; + } + + private class GetUpdateInfo implements Callable { + private final long mVersion; + + public GetUpdateInfo(long version) { + mVersion = version; + } + + public UpdateInfo call() throws Exception { + UpdateInfo info = new UpdateInfo(); + long version = mVersion; + info.version = mSourceVersion; + info.size = mSize; + long setVersion[] = mSetVersion; + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + int index = i % DATA_CACHE_SIZE; + if (setVersion[index] != version) { + info.reloadStart = i; + info.reloadCount = Math.min(MAX_LOAD_COUNT, n - i); + return info; + } + } + return mSourceVersion == mVersion ? null : info; + } + } + + private class UpdateContent implements Callable { + + private UpdateInfo mUpdateInfo; + + public UpdateContent(UpdateInfo info) { + mUpdateInfo = info; + } + + @Override + public Void call() throws Exception { + UpdateInfo info = mUpdateInfo; + mSourceVersion = info.version; + if (mSize != info.size) { + mSize = info.size; + if (mModelListener != null) mModelListener.onSizeChanged(mSize); + if (mContentEnd > mSize) mContentEnd = mSize; + if (mActiveEnd > mSize) mActiveEnd = mSize; + } + + ArrayList items = info.items; + + if (items == null) return null; + int start = Math.max(info.reloadStart, mContentStart); + int end = Math.min(info.reloadStart + items.size(), mContentEnd); + + for (int i = start; i < end; ++i) { + int index = i % DATA_CACHE_SIZE; + mSetVersion[index] = info.version; + MediaItem updateItem = items.get(i - info.reloadStart); + long itemVersion = updateItem.getDataVersion(); + if (mItemVersion[index] != itemVersion) { + mItemVersion[index] = itemVersion; + mData[index] = updateItem; + if (mModelListener != null && i >= mActiveStart && i < mActiveEnd) { + mModelListener.onWindowContentChanged(i); + } + } + } + return null; + } + } + + /* + * The thread model of ReloadTask + * * + * [Reload Task] [Main Thread] + * | | + * getUpdateInfo() --> | (synchronous call) + * (wait) <---- getUpdateInfo() + * | | + * Load Data | + * | | + * updateContent() --> | (synchronous call) + * (wait) updateContent() + * | | + * | | + */ + private class ReloadTask extends Thread { + + private volatile boolean mActive = true; + private volatile boolean mDirty = true; + private boolean mIsLoading = false; + + private void updateLoading(boolean loading) { + if (mIsLoading == loading) return; + mIsLoading = loading; + mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); + } + + @Override + public void run() { + boolean updateComplete = false; + while (mActive) { + synchronized (this) { + if (mActive && !mDirty && updateComplete) { + updateLoading(false); + Utils.waitWithoutInterrupt(this); + continue; + } + } + mDirty = false; + updateLoading(true); + long version; + synchronized (DataManager.LOCK) { + version = mSource.reload(); + } + UpdateInfo info = executeAndWait(new GetUpdateInfo(version)); + updateComplete = info == null; + if (updateComplete) continue; + synchronized (DataManager.LOCK) { + if (info.version != version) { + info.size = mSource.getMediaItemCount(); + info.version = version; + } + if (info.reloadCount > 0) { + info.items = mSource.getMediaItem(info.reloadStart, info.reloadCount); + } + } + executeAndWait(new UpdateContent(info)); + } + updateLoading(false); + } + + public synchronized void notifyDirty() { + mDirty = true; + notifyAll(); + } + + public synchronized void terminate() { + mActive = false; + notifyAll(); + } + } +} diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java new file mode 100644 index 000000000..5c09ce2d2 --- /dev/null +++ b/src/com/android/gallery3d/app/AlbumPage.java @@ -0,0 +1,602 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.MtpDevice; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.ActionModeHandler; +import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener; +import com.android.gallery3d.ui.AlbumView; +import com.android.gallery3d.ui.DetailsWindow; +import com.android.gallery3d.ui.DetailsWindow.CloseListener; +import com.android.gallery3d.ui.GLCanvas; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.GridDrawer; +import com.android.gallery3d.ui.HighlightDrawer; +import com.android.gallery3d.ui.PositionProvider; +import com.android.gallery3d.ui.PositionRepository; +import com.android.gallery3d.ui.PositionRepository.Position; +import com.android.gallery3d.ui.SelectionManager; +import com.android.gallery3d.ui.SlotView; +import com.android.gallery3d.ui.StaticBackground; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.GalleryUtils; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.provider.MediaStore; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View.MeasureSpec; +import android.widget.Toast; + +import java.util.Random; + +public class AlbumPage extends ActivityState implements GalleryActionBar.ClusterRunner, + SelectionManager.SelectionListener { + @SuppressWarnings("unused") + private static final String TAG = "AlbumPage"; + + public static final String KEY_MEDIA_PATH = "media-path"; + public static final String KEY_SET_CENTER = "set-center"; + public static final String KEY_AUTO_SELECT_ALL = "auto-select-all"; + public static final String KEY_SHOW_CLUSTER_MENU = "cluster-menu"; + + private static final int REQUEST_SLIDESHOW = 1; + private static final int REQUEST_PHOTO = 2; + private static final int REQUEST_DO_ANIMATION = 3; + + private static final float USER_DISTANCE_METER = 0.3f; + + private boolean mIsActive = false; + private StaticBackground mStaticBackground; + private AlbumView mAlbumView; + private Path mMediaSetPath; + + private AlbumDataAdapter mAlbumDataAdapter; + + protected SelectionManager mSelectionManager; + private GridDrawer mGridDrawer; + private HighlightDrawer mHighlightDrawer; + + private boolean mGetContent; + private boolean mShowClusterMenu; + + private ActionMode mActionMode; + private ActionModeHandler mActionModeHandler; + private int mFocusIndex = 0; + private DetailsWindow mDetailsWindow; + private MediaSet mMediaSet; + private boolean mShowDetails; + private float mUserDistance; // in pixel + + private ProgressDialog mProgressDialog; + private Future mPendingTask; + + private Future mSyncTask = null; + + private GLView mRootPane = new GLView() { + private float mMatrix[] = new float[16]; + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + mStaticBackground.layout(0, 0, right - left, bottom - top); + + int slotViewTop = GalleryActionBar.getHeight((Activity) mActivity); + int slotViewBottom = bottom - top; + int slotViewRight = right - left; + + if (mShowDetails) { + mDetailsWindow.measure( + MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int width = mDetailsWindow.getMeasuredWidth(); + int detailLeft = right - left - width; + slotViewRight = detailLeft; + mDetailsWindow.layout(detailLeft, slotViewTop, detailLeft + width, + bottom - top); + } else { + mAlbumView.setSelectionDrawer(mGridDrawer); + } + + mAlbumView.layout(0, slotViewTop, slotViewRight, slotViewBottom); + GalleryUtils.setViewPointMatrix(mMatrix, + (right - left) / 2, (bottom - top) / 2, -mUserDistance); + PositionRepository.getInstance(mActivity).setOffset( + 0, slotViewTop); + } + + @Override + protected void render(GLCanvas canvas) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + canvas.multiplyMatrix(mMatrix, 0); + super.render(canvas); + canvas.restore(); + } + }; + + @Override + protected void onBackPressed() { + if (mShowDetails) { + hideDetails(); + } else if (mSelectionManager.inSelectionMode()) { + mSelectionManager.leaveSelectionMode(); + } else { + mAlbumView.savePositions(PositionRepository.getInstance(mActivity)); + super.onBackPressed(); + } + } + + public void onSingleTapUp(int slotIndex) { + MediaItem item = mAlbumDataAdapter.get(slotIndex); + if (item == null) { + Log.w(TAG, "item not ready yet, ignore the click"); + return; + } + if (mShowDetails) { + mHighlightDrawer.setHighlightItem(item.getPath()); + mDetailsWindow.reloadDetails(slotIndex); + } else if (!mSelectionManager.inSelectionMode()) { + if (mGetContent) { + onGetContent(item); + } else { + boolean playVideo = + (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0; + if (playVideo) { + // Play the video. + PhotoPage.playVideo((Activity) mActivity, item.getPlayUri(), item.getName()); + } else { + // Get into the PhotoPage. + Bundle data = new Bundle(); + mAlbumView.savePositions(PositionRepository.getInstance(mActivity)); + data.putInt(PhotoPage.KEY_INDEX_HINT, slotIndex); + data.putString(PhotoPage.KEY_MEDIA_SET_PATH, + mMediaSetPath.toString()); + data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, + item.getPath().toString()); + mActivity.getStateManager().startStateForResult( + PhotoPage.class, REQUEST_PHOTO, data); + } + } + } else { + mSelectionManager.toggle(item.getPath()); + mAlbumView.invalidate(); + } + } + + private void onGetContent(final MediaItem item) { + DataManager dm = mActivity.getDataManager(); + Activity activity = (Activity) mActivity; + if (mData.getString(Gallery.EXTRA_CROP) != null) { + // TODO: Handle MtpImagew + Uri uri = dm.getContentUri(item.getPath()); + Intent intent = new Intent(CropImage.ACTION_CROP, uri) + .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) + .putExtras(getData()); + if (mData.getParcelable(MediaStore.EXTRA_OUTPUT) == null) { + intent.putExtra(CropImage.KEY_RETURN_DATA, true); + } + activity.startActivity(intent); + activity.finish(); + } else { + activity.setResult(Activity.RESULT_OK, + new Intent(null, item.getContentUri())); + activity.finish(); + } + } + + public void onLongTap(int slotIndex) { + if (mGetContent) return; + if (mShowDetails) { + onSingleTapUp(slotIndex); + } else { + MediaItem item = mAlbumDataAdapter.get(slotIndex); + if (item == null) return; + mSelectionManager.setAutoLeaveSelectionMode(true); + mSelectionManager.toggle(item.getPath()); + mAlbumView.invalidate(); + } + } + + public void doCluster(int clusterType) { + String basePath = mMediaSet.getPath().toString(); + String newPath = FilterUtils.newClusterPath(basePath, clusterType); + Bundle data = new Bundle(getData()); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath); + if (mShowClusterMenu) { + Context context = mActivity.getAndroidContext(); + data.putString(AlbumSetPage.KEY_SET_TITLE, mMediaSet.getName()); + data.putString(AlbumSetPage.KEY_SET_SUBTITLE, + GalleryActionBar.getClusterByTypeString(context, clusterType)); + } + + mAlbumView.savePositions(PositionRepository.getInstance(mActivity)); + mActivity.getStateManager().startStateForResult( + AlbumSetPage.class, REQUEST_DO_ANIMATION, data); + } + + public void doFilter(int filterType) { + String basePath = mMediaSet.getPath().toString(); + String newPath = FilterUtils.switchFilterPath(basePath, filterType); + Bundle data = new Bundle(getData()); + data.putString(AlbumPage.KEY_MEDIA_PATH, newPath); + mAlbumView.savePositions(PositionRepository.getInstance(mActivity)); + mActivity.getStateManager().switchState(this, AlbumPage.class, data); + } + + public void onOperationComplete() { + mAlbumView.invalidate(); + // TODO: enable animation + } + + @Override + protected void onCreate(Bundle data, Bundle restoreState) { + mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER); + initializeViews(); + initializeData(data); + mGetContent = data.getBoolean(Gallery.KEY_GET_CONTENT, false); + mShowClusterMenu = data.getBoolean(KEY_SHOW_CLUSTER_MENU, false); + + startTransition(data); + + // Enable auto-select-all for mtp album + if (data.getBoolean(KEY_AUTO_SELECT_ALL)) { + mSelectionManager.selectAll(); + } + } + + private void startTransition() { + final PositionRepository repository = + PositionRepository.getInstance(mActivity); + mAlbumView.startTransition(new PositionProvider() { + private Position mTempPosition = new Position(); + public Position getPosition(long identity, Position target) { + Position p = repository.get(identity); + if (p != null) return p; + mTempPosition.set(target); + mTempPosition.z = 128; + return mTempPosition; + } + }); + } + + private void startTransition(Bundle data) { + final PositionRepository repository = + PositionRepository.getInstance(mActivity); + final int[] center = data == null + ? null + : data.getIntArray(KEY_SET_CENTER); + final Random random = new Random(); + mAlbumView.startTransition(new PositionProvider() { + private Position mTempPosition = new Position(); + public Position getPosition(long identity, Position target) { + Position p = repository.get(identity); + if (p != null) return p; + if (center != null) { + random.setSeed(identity); + mTempPosition.set(center[0], center[1], + 0, random.nextInt(60) - 30, 0); + } else { + mTempPosition.set(target); + mTempPosition.z = 128; + } + return mTempPosition; + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + mIsActive = true; + setContentPane(mRootPane); + mAlbumDataAdapter.resume(); + mAlbumView.resume(); + mActionModeHandler.resume(); + } + + @Override + protected void onPause() { + super.onPause(); + mIsActive = false; + mAlbumDataAdapter.pause(); + mAlbumView.pause(); + if (mDetailsWindow != null) { + mDetailsWindow.pause(); + } + Future task = mPendingTask; + if (task != null) { + // cancel on going task + task.cancel(); + task.waitDone(); + if (mProgressDialog != null) { + mProgressDialog.dismiss(); + mProgressDialog = null; + } + } + if (mSyncTask != null) { + mSyncTask.cancel(); + mSyncTask = null; + } + mActionModeHandler.pause(); + } + + @Override + protected void onDestroy() { + if (mAlbumDataAdapter != null) { + mAlbumDataAdapter.setLoadingListener(null); + } + } + + private void initializeViews() { + mStaticBackground = new StaticBackground((Context) mActivity); + mRootPane.addComponent(mStaticBackground); + + mSelectionManager = new SelectionManager(mActivity, false); + mSelectionManager.setSelectionListener(this); + mGridDrawer = new GridDrawer((Context) mActivity, mSelectionManager); + Config.AlbumPage config = Config.AlbumPage.get((Context) mActivity); + mAlbumView = new AlbumView(mActivity, + config.slotWidth, config.slotHeight, config.displayItemSize); + mAlbumView.setSelectionDrawer(mGridDrawer); + mRootPane.addComponent(mAlbumView); + mAlbumView.setListener(new SlotView.SimpleListener() { + @Override + public void onSingleTapUp(int slotIndex) { + AlbumPage.this.onSingleTapUp(slotIndex); + } + @Override + public void onLongTap(int slotIndex) { + AlbumPage.this.onLongTap(slotIndex); + } + }); + mActionModeHandler = new ActionModeHandler(mActivity, mSelectionManager); + mActionModeHandler.setActionModeListener(new ActionModeListener() { + public boolean onActionItemClicked(MenuItem item) { + return onItemSelected(item); + } + }); + mStaticBackground.setImage(R.drawable.background, + R.drawable.background_portrait); + } + + private void initializeData(Bundle data) { + mMediaSetPath = Path.fromString(data.getString(KEY_MEDIA_PATH)); + mMediaSet = mActivity.getDataManager().getMediaSet(mMediaSetPath); + Utils.assertTrue(mMediaSet != null, + "MediaSet is null. Path = %s", mMediaSetPath); + mSelectionManager.setSourceMediaSet(mMediaSet); + mAlbumDataAdapter = new AlbumDataAdapter(mActivity, mMediaSet); + mAlbumDataAdapter.setLoadingListener(new MyLoadingListener()); + mAlbumView.setModel(mAlbumDataAdapter); + } + + private void showDetails() { + mShowDetails = true; + if (mDetailsWindow == null) { + mHighlightDrawer = new HighlightDrawer(mActivity.getAndroidContext()); + mDetailsWindow = new DetailsWindow(mActivity, new MyDetailsSource()); + mDetailsWindow.setCloseListener(new CloseListener() { + public void onClose() { + hideDetails(); + } + }); + mRootPane.addComponent(mDetailsWindow); + } + mAlbumView.setSelectionDrawer(mHighlightDrawer); + mDetailsWindow.show(); + } + + private void hideDetails() { + mShowDetails = false; + mAlbumView.setSelectionDrawer(mGridDrawer); + mDetailsWindow.hide(); + } + + @Override + protected boolean onCreateActionBar(Menu menu) { + Activity activity = (Activity) mActivity; + GalleryActionBar actionBar = mActivity.getGalleryActionBar(); + MenuInflater inflater = activity.getMenuInflater(); + + if (mGetContent) { + inflater.inflate(R.menu.pickup, menu); + int typeBits = mData.getInt(Gallery.KEY_TYPE_BITS, + DataManager.INCLUDE_IMAGE); + + actionBar.setTitle(GalleryUtils.getSelectionModePrompt(typeBits)); + } else { + inflater.inflate(R.menu.album, menu); + actionBar.setTitle(mMediaSet.getName()); + if (mMediaSet instanceof MtpDevice) { + menu.findItem(R.id.action_slideshow).setVisible(false); + } else { + menu.findItem(R.id.action_slideshow).setVisible(true); + } + + MenuItem groupBy = menu.findItem(R.id.action_group_by); + FilterUtils.setupMenuItems(actionBar, mMediaSetPath, true); + + if (groupBy != null) { + groupBy.setVisible(mShowClusterMenu); + } + + actionBar.setTitle(mMediaSet.getName()); + } + actionBar.setSubtitle(null); + + return true; + } + + @Override + protected boolean onItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_select: + mSelectionManager.setAutoLeaveSelectionMode(false); + mSelectionManager.enterSelectionMode(); + return true; + case R.id.action_group_by: { + mActivity.getGalleryActionBar().showClusterDialog(this); + return true; + } + case R.id.action_slideshow: { + Bundle data = new Bundle(); + data.putString(SlideshowPage.KEY_SET_PATH, + mMediaSetPath.toString()); + data.putBoolean(SlideshowPage.KEY_REPEAT, true); + mActivity.getStateManager().startStateForResult( + SlideshowPage.class, REQUEST_SLIDESHOW, data); + return true; + } + case R.id.action_details: { + if (mShowDetails) { + hideDetails(); + } else { + showDetails(); + } + return true; + } + default: + return false; + } + } + + @Override + protected void onStateResult(int request, int result, Intent data) { + switch (request) { + case REQUEST_SLIDESHOW: { + // data could be null, if there is no images in the album + if (data == null) return; + mFocusIndex = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0); + mAlbumView.setCenterIndex(mFocusIndex); + break; + } + case REQUEST_PHOTO: { + if (data == null) return; + mFocusIndex = data.getIntExtra(PhotoPage.KEY_INDEX_HINT, 0); + mAlbumView.setCenterIndex(mFocusIndex); + startTransition(); + break; + } + case REQUEST_DO_ANIMATION: { + startTransition(null); + break; + } + } + } + + public void onSelectionModeChange(int mode) { + switch (mode) { + case SelectionManager.ENTER_SELECTION_MODE: { + mActionMode = mActionModeHandler.startActionMode(); + break; + } + case SelectionManager.LEAVE_SELECTION_MODE: { + mActionMode.finish(); + mRootPane.invalidate(); + break; + } + case SelectionManager.SELECT_ALL_MODE: { + int count = mSelectionManager.getSelectedCount(); + String format = mActivity.getResources().getQuantityString( + R.plurals.number_of_items_selected, count); + mActionModeHandler.setTitle(String.format(format, count)); + mActionModeHandler.updateSupportedOperation(); + mRootPane.invalidate(); + break; + } + } + } + + public void onSelectionChange(Path path, boolean selected) { + Utils.assertTrue(mActionMode != null); + int count = mSelectionManager.getSelectedCount(); + String format = mActivity.getResources().getQuantityString( + R.plurals.number_of_items_selected, count); + mActionModeHandler.setTitle(String.format(format, count)); + mActionModeHandler.updateSupportedOperation(path, selected); + } + + private class MyLoadingListener implements LoadingListener { + @Override + public void onLoadingStarted() { + GalleryUtils.setSpinnerVisibility((Activity) mActivity, true); + } + + @Override + public void onLoadingFinished() { + if (!mIsActive) return; + if (mAlbumDataAdapter.size() == 0) { + if (mSyncTask == null) { + mSyncTask = mMediaSet.requestSync(); + } + if (mSyncTask.isDone()){ + Toast.makeText((Context) mActivity, + R.string.empty_album, Toast.LENGTH_LONG).show(); + mActivity.getStateManager().finishState(AlbumPage.this); + } + } + if (mSyncTask == null || mSyncTask.isDone()) { + GalleryUtils.setSpinnerVisibility((Activity) mActivity, false); + } + } + } + + private class MyDetailsSource implements DetailsWindow.DetailsSource { + private int mIndex; + public int size() { + return mAlbumDataAdapter.size(); + } + + // If requested index is out of active window, suggest a valid index. + // If there is no valid index available, return -1. + public int findIndex(int indexHint) { + if (mAlbumDataAdapter.isActive(indexHint)) { + mIndex = indexHint; + } else { + mIndex = mAlbumDataAdapter.getActiveStart(); + if (!mAlbumDataAdapter.isActive(mIndex)) { + return -1; + } + } + return mIndex; + } + + public MediaDetails getDetails() { + MediaObject item = mAlbumDataAdapter.get(mIndex); + if (item != null) { + mHighlightDrawer.setHighlightItem(item.getPath()); + return item.getDetails(); + } else { + return null; + } + } + } +} diff --git a/src/com/android/gallery3d/app/AlbumPicker.java b/src/com/android/gallery3d/app/AlbumPicker.java new file mode 100644 index 000000000..b86aee879 --- /dev/null +++ b/src/com/android/gallery3d/app/AlbumPicker.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.GLRootView; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; + +public class AlbumPicker extends AbstractGalleryActivity + implements OnClickListener { + + public static final String KEY_ALBUM_PATH = "album-path"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.dialog_picker); + ((GLRootView) findViewById(R.id.gl_root_view)).setZOrderOnTop(true); + findViewById(R.id.cancel).setOnClickListener(this); + setTitle(R.string.select_album); + Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + Bundle data = extras == null ? new Bundle() : new Bundle(extras); + + data.putBoolean(Gallery.KEY_GET_ALBUM, true); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, + getDataManager().getTopSetPath(DataManager.INCLUDE_IMAGE)); + getStateManager().startState(AlbumSetPage.class, data); + } + + @Override + public void onBackPressed() { + // send the back event to the top sub-state + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + getStateManager().getTopState().onBackPressed(); + } finally { + root.unlockRenderThread(); + } + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.cancel) finish(); + } +} diff --git a/src/com/android/gallery3d/app/AlbumSetDataAdapter.java b/src/com/android/gallery3d/app/AlbumSetDataAdapter.java new file mode 100644 index 000000000..9086ddbf4 --- /dev/null +++ b/src/com/android/gallery3d/app/AlbumSetDataAdapter.java @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.ui.AlbumSetView; +import com.android.gallery3d.ui.SynchronizedHandler; + +import android.os.Handler; +import android.os.Message; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +public class AlbumSetDataAdapter implements AlbumSetView.Model { + @SuppressWarnings("unused") + private static final String TAG = "AlbumSetDataAdapter"; + + private static final int INDEX_NONE = -1; + + private static final int MIN_LOAD_COUNT = 4; + private static final int MAX_COVER_COUNT = 4; + + private static final int MSG_LOAD_START = 1; + private static final int MSG_LOAD_FINISH = 2; + private static final int MSG_RUN_OBJECT = 3; + + private static final MediaItem[] EMPTY_MEDIA_ITEMS = new MediaItem[0]; + + private final MediaSet[] mData; + private final MediaItem[][] mCoverData; + private final long[] mItemVersion; + private final long[] mSetVersion; + + private int mActiveStart = 0; + private int mActiveEnd = 0; + + private int mContentStart = 0; + private int mContentEnd = 0; + + private final MediaSet mSource; + private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; + private int mSize; + + private AlbumSetView.ModelListener mModelListener; + private LoadingListener mLoadingListener; + private ReloadTask mReloadTask; + + private final Handler mMainHandler; + + private MySourceListener mSourceListener = new MySourceListener(); + + public AlbumSetDataAdapter(GalleryActivity activity, MediaSet albumSet, int cacheSize) { + mSource = Utils.checkNotNull(albumSet); + mCoverData = new MediaItem[cacheSize][]; + mData = new MediaSet[cacheSize]; + mItemVersion = new long[cacheSize]; + mSetVersion = new long[cacheSize]; + Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION); + Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION); + + mMainHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_RUN_OBJECT: + ((Runnable) message.obj).run(); + return; + case MSG_LOAD_START: + if (mLoadingListener != null) mLoadingListener.onLoadingStarted(); + return; + case MSG_LOAD_FINISH: + if (mLoadingListener != null) mLoadingListener.onLoadingFinished(); + return; + } + } + }; + } + + public void pause() { + mReloadTask.terminate(); + mReloadTask = null; + mSource.removeContentListener(mSourceListener); + } + + public void resume() { + mSource.addContentListener(mSourceListener); + mReloadTask = new ReloadTask(); + mReloadTask.start(); + } + + public MediaSet getMediaSet(int index) { + if (index < mActiveStart && index >= mActiveEnd) { + throw new IllegalArgumentException(String.format( + "%s not in (%s, %s)", index, mActiveStart, mActiveEnd)); + } + return mData[index % mData.length]; + } + + public MediaItem[] getCoverItems(int index) { + if (index < mActiveStart && index >= mActiveEnd) { + throw new IllegalArgumentException(String.format( + "%s not in (%s, %s)", index, mActiveStart, mActiveEnd)); + } + MediaItem[] result = mCoverData[index % mCoverData.length]; + + // If the result is not ready yet, return an empty array + return result == null ? EMPTY_MEDIA_ITEMS : result; + } + + public int getActiveStart() { + return mActiveStart; + } + + public int getActiveEnd() { + return mActiveEnd; + } + + public boolean isActive(int index) { + return index >= mActiveStart && index < mActiveEnd; + } + + public int size() { + return mSize; + } + + private void clearSlot(int slotIndex) { + mData[slotIndex] = null; + mCoverData[slotIndex] = null; + mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; + mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; + } + + private void setContentWindow(int contentStart, int contentEnd) { + if (contentStart == mContentStart && contentEnd == mContentEnd) return; + MediaItem[][] data = mCoverData; + int length = data.length; + + int start = this.mContentStart; + int end = this.mContentEnd; + + mContentStart = contentStart; + mContentEnd = contentEnd; + + if (contentStart >= end || start >= contentEnd) { + for (int i = start, n = end; i < n; ++i) { + clearSlot(i % length); + } + } else { + for (int i = start; i < contentStart; ++i) { + clearSlot(i % length); + } + for (int i = contentEnd, n = end; i < n; ++i) { + clearSlot(i % length); + } + } + mReloadTask.notifyDirty(); + } + + public void setActiveWindow(int start, int end) { + if (start == mActiveStart && end == mActiveEnd) return; + + Utils.assertTrue(start <= end + && end - start <= mCoverData.length && end <= mSize); + + mActiveStart = start; + mActiveEnd = end; + + int length = mCoverData.length; + // If no data is visible, keep the cache content + if (start == end) return; + + int contentStart = Utils.clamp((start + end) / 2 - length / 2, + 0, Math.max(0, mSize - length)); + int contentEnd = Math.min(contentStart + length, mSize); + if (mContentStart > start || mContentEnd < end + || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) { + setContentWindow(contentStart, contentEnd); + } + } + + private class MySourceListener implements ContentListener { + public void onContentDirty() { + mReloadTask.notifyDirty(); + } + } + + public void setModelListener(AlbumSetView.ModelListener listener) { + mModelListener = listener; + } + + public void setLoadingListener(LoadingListener listener) { + mLoadingListener = listener; + } + + private static void getRepresentativeItems(MediaSet set, int wanted, + ArrayList result) { + if (set.getMediaItemCount() > 0) { + result.addAll(set.getMediaItem(0, wanted)); + } + + int n = set.getSubMediaSetCount(); + for (int i = 0; i < n && wanted > result.size(); i++) { + MediaSet subset = set.getSubMediaSet(i); + double perSet = (double) (wanted - result.size()) / (n - i); + int m = (int) Math.ceil(perSet); + getRepresentativeItems(subset, m, result); + } + } + + private static class UpdateInfo { + public long version; + public int index; + + public int size; + public MediaSet item; + public MediaItem covers[]; + } + + private class GetUpdateInfo implements Callable { + + private final long mVersion; + + public GetUpdateInfo(long version) { + mVersion = version; + } + + private int getInvalidIndex(long version) { + long setVersion[] = mSetVersion; + int length = setVersion.length; + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + int index = i % length; + if (setVersion[i % length] != version) return i; + } + return INDEX_NONE; + } + + @Override + public UpdateInfo call() throws Exception { + int index = getInvalidIndex(mVersion); + if (index == INDEX_NONE + && mSourceVersion == mVersion) return null; + UpdateInfo info = new UpdateInfo(); + info.version = mSourceVersion; + info.index = index; + info.size = mSize; + return info; + } + } + + private class UpdateContent implements Callable { + private UpdateInfo mUpdateInfo; + + public UpdateContent(UpdateInfo info) { + mUpdateInfo = info; + } + + public Void call() { + UpdateInfo info = mUpdateInfo; + mSourceVersion = info.version; + if (mSize != info.size) { + mSize = info.size; + if (mModelListener != null) mModelListener.onSizeChanged(mSize); + if (mContentEnd > mSize) mContentEnd = mSize; + if (mActiveEnd > mSize) mActiveEnd = mSize; + } + // Note: info.index could be INDEX_NONE, i.e., -1 + if (info.index >= mContentStart && info.index < mContentEnd) { + int pos = info.index % mCoverData.length; + mSetVersion[pos] = info.version; + long itemVersion = info.item.getDataVersion(); + if (mItemVersion[pos] == itemVersion) return null; + mItemVersion[pos] = itemVersion; + mData[pos] = info.item; + mCoverData[pos] = info.covers; + if (mModelListener != null + && info.index >= mActiveStart && info.index < mActiveEnd) { + mModelListener.onWindowContentChanged(info.index); + } + } + return null; + } + } + + private T executeAndWait(Callable callable) { + FutureTask task = new FutureTask(callable); + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); + try { + return task.get(); + } catch (InterruptedException e) { + return null; + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + // TODO: load active range first + private class ReloadTask extends Thread { + private volatile boolean mActive = true; + private volatile boolean mDirty = true; + private volatile boolean mIsLoading = false; + + private void updateLoading(boolean loading) { + if (mIsLoading == loading) return; + mIsLoading = loading; + mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); + } + + @Override + public void run() { + boolean updateComplete = false; + while (mActive) { + synchronized (this) { + if (mActive && !mDirty && updateComplete) { + updateLoading(false); + Utils.waitWithoutInterrupt(this); + continue; + } + } + mDirty = false; + updateLoading(true); + + long version; + synchronized (DataManager.LOCK) { + version = mSource.reload(); + } + UpdateInfo info = executeAndWait(new GetUpdateInfo(version)); + updateComplete = info == null; + if (updateComplete) continue; + + synchronized (DataManager.LOCK) { + if (info.version != version) { + info.version = version; + info.size = mSource.getSubMediaSetCount(); + } + if (info.index != INDEX_NONE) { + info.item = mSource.getSubMediaSet(info.index); + if (info.item == null) continue; + ArrayList covers = new ArrayList(); + getRepresentativeItems(info.item, MAX_COVER_COUNT, covers); + info.covers = covers.toArray(new MediaItem[covers.size()]); + } + } + executeAndWait(new UpdateContent(info)); + } + updateLoading(false); + } + + public synchronized void notifyDirty() { + mDirty = true; + notifyAll(); + } + + public synchronized void terminate() { + mActive = false; + notifyAll(); + } + } +} + + diff --git a/src/com/android/gallery3d/app/AlbumSetPage.java b/src/com/android/gallery3d/app/AlbumSetPage.java new file mode 100644 index 000000000..688ff81f2 --- /dev/null +++ b/src/com/android/gallery3d/app/AlbumSetPage.java @@ -0,0 +1,586 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.picasasource.PicasaSource; +import com.android.gallery3d.settings.GallerySettings; +import com.android.gallery3d.ui.ActionModeHandler; +import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener; +import com.android.gallery3d.ui.AlbumSetView; +import com.android.gallery3d.ui.DetailsWindow; +import com.android.gallery3d.ui.DetailsWindow.CloseListener; +import com.android.gallery3d.ui.GLCanvas; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.GridDrawer; +import com.android.gallery3d.ui.HighlightDrawer; +import com.android.gallery3d.ui.PositionProvider; +import com.android.gallery3d.ui.PositionRepository; +import com.android.gallery3d.ui.PositionRepository.Position; +import com.android.gallery3d.ui.SelectionManager; +import com.android.gallery3d.ui.SlotView; +import com.android.gallery3d.ui.StaticBackground; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.util.GalleryUtils; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Message; +import android.provider.MediaStore; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View.MeasureSpec; +import android.widget.Toast; + +public class AlbumSetPage extends ActivityState implements + SelectionManager.SelectionListener, GalleryActionBar.ClusterRunner, + EyePosition.EyePositionListener { + @SuppressWarnings("unused") + private static final String TAG = "AlbumSetPage"; + + public static final String KEY_MEDIA_PATH = "media-path"; + public static final String KEY_SET_TITLE = "set-title"; + public static final String KEY_SET_SUBTITLE = "set-subtitle"; + private static final int DATA_CACHE_SIZE = 256; + private static final int REQUEST_DO_ANIMATION = 1; + private static final int MSG_GOTO_MANAGE_CACHE_PAGE = 1; + + private boolean mIsActive = false; + private StaticBackground mStaticBackground; + private AlbumSetView mAlbumSetView; + + private MediaSet mMediaSet; + private String mTitle; + private String mSubtitle; + private boolean mShowClusterTabs; + + protected SelectionManager mSelectionManager; + private AlbumSetDataAdapter mAlbumSetDataAdapter; + private GridDrawer mGridDrawer; + private HighlightDrawer mHighlightDrawer; + + private boolean mGetContent; + private boolean mGetAlbum; + private ActionMode mActionMode; + private ActionModeHandler mActionModeHandler; + private DetailsWindow mDetailsWindow; + private boolean mShowDetails; + private EyePosition mEyePosition; + + // The eyes' position of the user, the origin is at the center of the + // device and the unit is in pixels. + private float mX; + private float mY; + private float mZ; + + private SynchronizedHandler mHandler; + + private GLView mRootPane = new GLView() { + private float mMatrix[] = new float[16]; + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + mStaticBackground.layout(0, 0, right - left, bottom - top); + mEyePosition.resetPosition(); + + int slotViewTop = GalleryActionBar.getHeight((Activity) mActivity); + int slotViewBottom = bottom - top; + int slotViewRight = right - left; + + if (mShowDetails) { + mDetailsWindow.measure( + MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int width = mDetailsWindow.getMeasuredWidth(); + int detailLeft = right - left - width; + slotViewRight = detailLeft; + mDetailsWindow.layout(detailLeft, slotViewTop, detailLeft + width, + bottom - top); + } else { + mAlbumSetView.setSelectionDrawer(mGridDrawer); + } + + mAlbumSetView.layout(0, slotViewTop, slotViewRight, slotViewBottom); + PositionRepository.getInstance(mActivity).setOffset( + 0, slotViewTop); + } + + @Override + protected void render(GLCanvas canvas) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + GalleryUtils.setViewPointMatrix(mMatrix, + getWidth() / 2 + mX, getHeight() / 2 + mY, mZ); + canvas.multiplyMatrix(mMatrix, 0); + super.render(canvas); + canvas.restore(); + } + }; + + @Override + public void onEyePositionChanged(float x, float y, float z) { + mRootPane.lockRendering(); + mX = x; + mY = y; + mZ = z; + mRootPane.unlockRendering(); + mRootPane.invalidate(); + } + + @Override + public void onBackPressed() { + if (mShowDetails) { + hideDetails(); + } else if (mSelectionManager.inSelectionMode()) { + mSelectionManager.leaveSelectionMode(); + } else { + mAlbumSetView.savePositions( + PositionRepository.getInstance(mActivity)); + super.onBackPressed(); + } + } + + private void savePositions(int slotIndex, int center[]) { + Rect offset = new Rect(); + mRootPane.getBoundsOf(mAlbumSetView, offset); + mAlbumSetView.savePositions(PositionRepository.getInstance(mActivity)); + Rect r = mAlbumSetView.getSlotRect(slotIndex); + int scrollX = mAlbumSetView.getScrollX(); + int scrollY = mAlbumSetView.getScrollY(); + center[0] = offset.left + (r.left + r.right) / 2 - scrollX; + center[1] = offset.top + (r.top + r.bottom) / 2 - scrollY; + } + + public void onSingleTapUp(int slotIndex) { + MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex); + if (targetSet == null) return; // Content is dirty, we shall reload soon + + if (mShowDetails) { + Path path = targetSet.getPath(); + mHighlightDrawer.setHighlightItem(path); + mDetailsWindow.reloadDetails(slotIndex); + } else if (!mSelectionManager.inSelectionMode()) { + Bundle data = new Bundle(getData()); + String mediaPath = targetSet.getPath().toString(); + int[] center = new int[2]; + savePositions(slotIndex, center); + data.putIntArray(AlbumPage.KEY_SET_CENTER, center); + if (mGetAlbum && targetSet.isLeafAlbum()) { + Activity activity = (Activity) mActivity; + Intent result = new Intent() + .putExtra(AlbumPicker.KEY_ALBUM_PATH, targetSet.getPath().toString()); + activity.setResult(Activity.RESULT_OK, result); + activity.finish(); + } else if (targetSet.getSubMediaSetCount() > 0) { + data.putString(AlbumSetPage.KEY_MEDIA_PATH, mediaPath); + mActivity.getStateManager().startStateForResult( + AlbumSetPage.class, REQUEST_DO_ANIMATION, data); + } else { + if (!mGetContent && (targetSet.getSupportedOperations() + & MediaObject.SUPPORT_IMPORT) != 0) { + data.putBoolean(AlbumPage.KEY_AUTO_SELECT_ALL, true); + } + data.putString(AlbumPage.KEY_MEDIA_PATH, mediaPath); + boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class); + // We only show cluster menu in the first AlbumPage in stack + data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum); + mActivity.getStateManager().startStateForResult( + AlbumPage.class, REQUEST_DO_ANIMATION, data); + } + } else { + mSelectionManager.toggle(targetSet.getPath()); + mAlbumSetView.invalidate(); + } + } + + public void onLongTap(int slotIndex) { + if (mGetContent || mGetAlbum) return; + if (mShowDetails) { + onSingleTapUp(slotIndex); + } else { + MediaSet set = mAlbumSetDataAdapter.getMediaSet(slotIndex); + if (set == null) return; + mSelectionManager.setAutoLeaveSelectionMode(true); + mSelectionManager.toggle(set.getPath()); + mAlbumSetView.invalidate(); + } + } + + public void doCluster(int clusterType) { + String basePath = mMediaSet.getPath().toString(); + String newPath = FilterUtils.switchClusterPath(basePath, clusterType); + Bundle data = new Bundle(getData()); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath); + mAlbumSetView.savePositions(PositionRepository.getInstance(mActivity)); + mActivity.getStateManager().switchState(this, AlbumSetPage.class, data); + } + + public void doFilter(int filterType) { + String basePath = mMediaSet.getPath().toString(); + String newPath = FilterUtils.switchFilterPath(basePath, filterType); + Bundle data = new Bundle(getData()); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath); + mAlbumSetView.savePositions(PositionRepository.getInstance(mActivity)); + mActivity.getStateManager().switchState(this, AlbumSetPage.class, data); + } + + public void onOperationComplete() { + mAlbumSetView.invalidate(); + // TODO: enable animation + } + + @Override + public void onCreate(Bundle data, Bundle restoreState) { + mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + Utils.assertTrue(message.what == MSG_GOTO_MANAGE_CACHE_PAGE); + Bundle data = new Bundle(); + String mediaPath = mActivity.getDataManager().getTopSetPath( + DataManager.INCLUDE_ALL); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, mediaPath); + mActivity.getStateManager().startState(ManageCachePage.class, data); + } + }; + + initializeViews(); + initializeData(data); + mGetContent = data.getBoolean(Gallery.KEY_GET_CONTENT, false); + mGetAlbum = data.getBoolean(Gallery.KEY_GET_ALBUM, false); + mTitle = data.getString(AlbumSetPage.KEY_SET_TITLE); + mSubtitle = data.getString(AlbumSetPage.KEY_SET_SUBTITLE); + mEyePosition = new EyePosition(mActivity.getAndroidContext(), this); + + startTransition(); + } + + @Override + public void onPause() { + super.onPause(); + mIsActive = false; + mActionModeHandler.pause(); + mAlbumSetDataAdapter.pause(); + mAlbumSetView.pause(); + mEyePosition.pause(); + if (mDetailsWindow != null) { + mDetailsWindow.pause(); + } + GalleryActionBar actionBar = mActivity.getGalleryActionBar(); + if (actionBar != null) actionBar.hideClusterTabs(); + } + + @Override + public void onResume() { + super.onResume(); + mIsActive = true; + setContentPane(mRootPane); + mAlbumSetDataAdapter.resume(); + mAlbumSetView.resume(); + mEyePosition.resume(); + mActionModeHandler.resume(); + GalleryActionBar actionBar = mActivity.getGalleryActionBar(); + if (mShowClusterTabs && actionBar != null) actionBar.showClusterTabs(this); + } + + private void initializeData(Bundle data) { + String mediaPath = data.getString(AlbumSetPage.KEY_MEDIA_PATH); + mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath); + mSelectionManager.setSourceMediaSet(mMediaSet); + mAlbumSetDataAdapter = new AlbumSetDataAdapter( + mActivity, mMediaSet, DATA_CACHE_SIZE); + mAlbumSetDataAdapter.setLoadingListener(new MyLoadingListener()); + mAlbumSetView.setModel(mAlbumSetDataAdapter); + } + + private void initializeViews() { + mSelectionManager = new SelectionManager(mActivity, true); + mSelectionManager.setSelectionListener(this); + mStaticBackground = new StaticBackground(mActivity.getAndroidContext()); + mRootPane.addComponent(mStaticBackground); + + mGridDrawer = new GridDrawer((Context) mActivity, mSelectionManager); + Config.AlbumSetPage config = Config.AlbumSetPage.get((Context) mActivity); + mAlbumSetView = new AlbumSetView(mActivity, mGridDrawer, + config.slotWidth, config.slotHeight, + config.displayItemSize, config.labelFontSize, + config.labelOffsetY, config.labelMargin); + mAlbumSetView.setListener(new SlotView.SimpleListener() { + @Override + public void onSingleTapUp(int slotIndex) { + AlbumSetPage.this.onSingleTapUp(slotIndex); + } + @Override + public void onLongTap(int slotIndex) { + AlbumSetPage.this.onLongTap(slotIndex); + } + }); + + mActionModeHandler = new ActionModeHandler(mActivity, mSelectionManager); + mActionModeHandler.setActionModeListener(new ActionModeListener() { + public boolean onActionItemClicked(MenuItem item) { + return onItemSelected(item); + } + }); + mRootPane.addComponent(mAlbumSetView); + + mStaticBackground.setImage(R.drawable.background, + R.drawable.background_portrait); + } + + @Override + protected boolean onCreateActionBar(Menu menu) { + Activity activity = (Activity) mActivity; + GalleryActionBar actionBar = mActivity.getGalleryActionBar(); + MenuInflater inflater = activity.getMenuInflater(); + + final boolean inAlbum = mActivity.getStateManager().hasStateClass( + AlbumPage.class); + + if (mGetContent) { + inflater.inflate(R.menu.pickup, menu); + int typeBits = mData.getInt( + Gallery.KEY_TYPE_BITS, DataManager.INCLUDE_IMAGE); + int id = R.string.select_image; + if ((typeBits & DataManager.INCLUDE_VIDEO) != 0) { + id = (typeBits & DataManager.INCLUDE_IMAGE) == 0 + ? R.string.select_video + : R.string.select_item; + } + actionBar.setTitle(id); + } else if (mGetAlbum) { + inflater.inflate(R.menu.pickup, menu); + actionBar.setTitle(R.string.select_album); + } else { + mShowClusterTabs = !inAlbum; + inflater.inflate(R.menu.albumset, menu); + if (mTitle != null) { + actionBar.setTitle(mTitle); + } else { + actionBar.setTitle(activity.getApplicationInfo().labelRes); + } + MenuItem selectItem = menu.findItem(R.id.action_select); + + if (selectItem != null) { + boolean selectAlbums = !inAlbum && + actionBar.getClusterTypeAction() == FilterUtils.CLUSTER_BY_ALBUM; + if (selectAlbums) { + selectItem.setTitle(R.string.select_album); + } else { + selectItem.setTitle(R.string.select_group); + } + } + + MenuItem switchCamera = menu.findItem(R.id.action_camera); + if (switchCamera != null) { + switchCamera.setVisible(GalleryUtils.isCameraAvailable(activity)); + } + + actionBar.setSubtitle(mSubtitle); + } + return true; + } + + @Override + protected boolean onItemSelected(MenuItem item) { + Activity activity = (Activity) mActivity; + switch (item.getItemId()) { + case R.id.action_select: + mSelectionManager.setAutoLeaveSelectionMode(false); + mSelectionManager.enterSelectionMode(); + return true; + case R.id.action_details: + if (mAlbumSetDataAdapter.size() != 0) { + if (mShowDetails) { + hideDetails(); + } else { + showDetails(); + } + } else { + Toast.makeText(activity, + activity.getText(R.string.no_albums_alert), + Toast.LENGTH_SHORT).show(); + } + return true; + case R.id.action_camera: { + Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA) + .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_NEW_TASK); + activity.startActivity(intent); + return true; + } + case R.id.action_manage_offline: { + mHandler.sendEmptyMessage(MSG_GOTO_MANAGE_CACHE_PAGE); + return true; + } + case R.id.action_sync_picasa_albums: { + PicasaSource.requestSync(activity); + return true; + } + case R.id.action_settings: { + activity.startActivity(new Intent(activity, GallerySettings.class)); + return true; + } + default: + return false; + } + } + + @Override + protected void onStateResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_DO_ANIMATION: { + startTransition(); + } + } + } + + private void startTransition() { + final PositionRepository repository = + PositionRepository.getInstance(mActivity); + mAlbumSetView.startTransition(new PositionProvider() { + private Position mTempPosition = new Position(); + public Position getPosition(long identity, Position target) { + Position p = repository.get(identity); + if (p == null) { + p = mTempPosition; + p.set(target.x, target.y, 128, target.theta, 1); + } + return p; + } + }); + } + + private String getSelectedString() { + GalleryActionBar actionBar = mActivity.getGalleryActionBar(); + int count = mSelectionManager.getSelectedCount(); + int action = actionBar.getClusterTypeAction(); + int string = action == FilterUtils.CLUSTER_BY_ALBUM + ? R.plurals.number_of_albums_selected + : R.plurals.number_of_groups_selected; + String format = mActivity.getResources().getQuantityString(string, count); + return String.format(format, count); + } + + public void onSelectionModeChange(int mode) { + + switch (mode) { + case SelectionManager.ENTER_SELECTION_MODE: { + mActivity.getGalleryActionBar().hideClusterTabs(); + mActionMode = mActionModeHandler.startActionMode(); + break; + } + case SelectionManager.LEAVE_SELECTION_MODE: { + mActionMode.finish(); + mActivity.getGalleryActionBar().showClusterTabs(this); + mRootPane.invalidate(); + break; + } + case SelectionManager.SELECT_ALL_MODE: { + mActionModeHandler.setTitle(getSelectedString()); + mRootPane.invalidate(); + break; + } + } + } + + public void onSelectionChange(Path path, boolean selected) { + Utils.assertTrue(mActionMode != null); + mActionModeHandler.setTitle(getSelectedString()); + mActionModeHandler.updateSupportedOperation(path, selected); + } + + private void hideDetails() { + mShowDetails = false; + mAlbumSetView.setSelectionDrawer(mGridDrawer); + mDetailsWindow.hide(); + } + + private void showDetails() { + mShowDetails = true; + if (mDetailsWindow == null) { + mHighlightDrawer = new HighlightDrawer(mActivity.getAndroidContext()); + mDetailsWindow = new DetailsWindow(mActivity, new MyDetailsSource()); + mDetailsWindow.setCloseListener(new CloseListener() { + public void onClose() { + hideDetails(); + } + }); + mRootPane.addComponent(mDetailsWindow); + } + mAlbumSetView.setSelectionDrawer(mHighlightDrawer); + mDetailsWindow.show(); + } + + private class MyLoadingListener implements LoadingListener { + public void onLoadingStarted() { + GalleryUtils.setSpinnerVisibility((Activity) mActivity, true); + } + + public void onLoadingFinished() { + if (!mIsActive) return; + GalleryUtils.setSpinnerVisibility((Activity) mActivity, false); + if (mAlbumSetDataAdapter.size() == 0) { + Toast.makeText((Context) mActivity, + R.string.empty_album, Toast.LENGTH_LONG).show(); + if (mActivity.getStateManager().getStateCount() > 1) { + mActivity.getStateManager().finishState(AlbumSetPage.this); + } + } + } + } + + private class MyDetailsSource implements DetailsWindow.DetailsSource { + private int mIndex; + public int size() { + return mAlbumSetDataAdapter.size(); + } + + // If requested index is out of active window, suggest a valid index. + // If there is no valid index available, return -1. + public int findIndex(int indexHint) { + if (mAlbumSetDataAdapter.isActive(indexHint)) { + mIndex = indexHint; + } else { + mIndex = mAlbumSetDataAdapter.getActiveStart(); + if (!mAlbumSetDataAdapter.isActive(mIndex)) { + return -1; + } + } + return mIndex; + } + + public MediaDetails getDetails() { + MediaObject item = mAlbumSetDataAdapter.getMediaSet(mIndex); + if (item != null) { + mHighlightDrawer.setHighlightItem(item.getPath()); + return item.getDetails(); + } else { + return null; + } + } + } +} diff --git a/src/com/android/gallery3d/app/Config.java b/src/com/android/gallery3d/app/Config.java new file mode 100644 index 000000000..4586235f6 --- /dev/null +++ b/src/com/android/gallery3d/app/Config.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.R; + +import android.content.Context; +import android.content.res.Resources; + +final class Config { + public static class AlbumSetPage { + private static AlbumSetPage sInstance; + + public final int slotWidth; + public final int slotHeight; + public final int displayItemSize; + public final int labelFontSize; + public final int labelOffsetY; + public final int labelMargin; + + public static synchronized AlbumSetPage get(Context context) { + if (sInstance == null) { + sInstance = new AlbumSetPage(context); + } + return sInstance; + } + + private AlbumSetPage(Context context) { + Resources r = context.getResources(); + slotWidth = r.getDimensionPixelSize(R.dimen.albumset_slot_width); + slotHeight = r.getDimensionPixelSize(R.dimen.albumset_slot_height); + displayItemSize = r.getDimensionPixelSize(R.dimen.albumset_display_item_size); + labelFontSize = r.getDimensionPixelSize(R.dimen.albumset_label_font_size); + labelOffsetY = r.getDimensionPixelSize(R.dimen.albumset_label_offset_y); + labelMargin = r.getDimensionPixelSize(R.dimen.albumset_label_margin); + } + } + + public static class AlbumPage { + private static AlbumPage sInstance; + + public final int slotWidth; + public final int slotHeight; + public final int displayItemSize; + + public static synchronized AlbumPage get(Context context) { + if (sInstance == null) { + sInstance = new AlbumPage(context); + } + return sInstance; + } + + private AlbumPage(Context context) { + Resources r = context.getResources(); + slotWidth = r.getDimensionPixelSize(R.dimen.album_slot_width); + slotHeight = r.getDimensionPixelSize(R.dimen.album_slot_height); + displayItemSize = r.getDimensionPixelSize(R.dimen.album_display_item_size); + } + } + + public static class ManageCachePage extends AlbumSetPage { + private static ManageCachePage sInstance; + + public final int cacheBarHeight; + public final int cacheBarPinLeftMargin; + public final int cacheBarPinRightMargin; + public final int cacheBarButtonRightMargin; + public final int cacheBarFontSize; + + public static synchronized ManageCachePage get(Context context) { + if (sInstance == null) { + sInstance = new ManageCachePage(context); + } + return sInstance; + } + + public ManageCachePage(Context context) { + super(context); + Resources r = context.getResources(); + cacheBarHeight = r.getDimensionPixelSize(R.dimen.cache_bar_height); + cacheBarPinLeftMargin = r.getDimensionPixelSize(R.dimen.cache_bar_pin_left_margin); + cacheBarPinRightMargin = r.getDimensionPixelSize( + R.dimen.cache_bar_pin_right_margin); + cacheBarButtonRightMargin = r.getDimensionPixelSize( + R.dimen.cache_bar_button_right_margin); + cacheBarFontSize = r.getDimensionPixelSize(R.dimen.cache_bar_font_size); + } + } + + public static class PhotoPage { + private static PhotoPage sInstance; + + // These are all height values. See the comment in FilmStripView for + // the meaning of these values. + public final int filmstripTopMargin; + public final int filmstripMidMargin; + public final int filmstripBottomMargin; + public final int filmstripThumbSize; + public final int filmstripContentSize; + public final int filmstripGripSize; + public final int filmstripBarSize; + + // These are width values. + public final int filmstripGripWidth; + + public static synchronized PhotoPage get(Context context) { + if (sInstance == null) { + sInstance = new PhotoPage(context); + } + return sInstance; + } + + public PhotoPage(Context context) { + Resources r = context.getResources(); + filmstripTopMargin = r.getDimensionPixelSize(R.dimen.filmstrip_top_margin); + filmstripMidMargin = r.getDimensionPixelSize(R.dimen.filmstrip_mid_margin); + filmstripBottomMargin = r.getDimensionPixelSize(R.dimen.filmstrip_bottom_margin); + filmstripThumbSize = r.getDimensionPixelSize(R.dimen.filmstrip_thumb_size); + filmstripContentSize = r.getDimensionPixelSize(R.dimen.filmstrip_content_size); + filmstripGripSize = r.getDimensionPixelSize(R.dimen.filmstrip_grip_size); + filmstripBarSize = r.getDimensionPixelSize(R.dimen.filmstrip_bar_size); + filmstripGripWidth = r.getDimensionPixelSize(R.dimen.filmstrip_grip_width); + } + } +} + diff --git a/src/com/android/gallery3d/app/CropImage.java b/src/com/android/gallery3d/app/CropImage.java new file mode 100644 index 000000000..6c0a0c7eb --- /dev/null +++ b/src/com/android/gallery3d/app/CropImage.java @@ -0,0 +1,850 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.LocalImage; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.picasasource.PicasaSource; +import com.android.gallery3d.ui.BitmapTileProvider; +import com.android.gallery3d.ui.CropView; +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.ui.TileImageViewAdapter; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.InterruptableOutputStream; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.app.ProgressDialog; +import android.app.WallpaperManager; +import android.content.ContentValues; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; +import android.provider.MediaStore; +import android.provider.MediaStore.Images; +import android.view.Menu; +import android.view.MenuItem; +import android.view.Window; +import android.widget.Toast; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * The activity can crop specific region of interest from an image. + */ +public class CropImage extends AbstractGalleryActivity { + private static final String TAG = "CropImage"; + public static final String ACTION_CROP = "com.android.camera.action.CROP"; + + private static final int MAX_PIXEL_COUNT = 5 * 1000000; // 5M pixels + private static final int MAX_FILE_INDEX = 1000; + private static final int TILE_SIZE = 512; + private static final int BACKUP_PIXEL_COUNT = 480000; // around 800x600 + + private static final int MSG_LARGE_BITMAP = 1; + private static final int MSG_BITMAP = 2; + private static final int MSG_SAVE_COMPLETE = 3; + + private static final int MAX_BACKUP_IMAGE_SIZE = 320; + private static final int DEFAULT_COMPRESS_QUALITY = 90; + + public static final String KEY_RETURN_DATA = "return-data"; + public static final String KEY_CROPPED_RECT = "cropped-rect"; + public static final String KEY_ASPECT_X = "aspectX"; + public static final String KEY_ASPECT_Y = "aspectY"; + public static final String KEY_SPOTLIGHT_X = "spotlightX"; + public static final String KEY_SPOTLIGHT_Y = "spotlightY"; + public static final String KEY_OUTPUT_X = "outputX"; + public static final String KEY_OUTPUT_Y = "outputY"; + public static final String KEY_SCALE = "scale"; + public static final String KEY_DATA = "data"; + public static final String KEY_SCALE_UP_IF_NEEDED = "scaleUpIfNeeded"; + public static final String KEY_OUTPUT_FORMAT = "outputFormat"; + public static final String KEY_SET_AS_WALLPAPER = "set-as-wallpaper"; + public static final String KEY_NO_FACE_DETECTION = "noFaceDetection"; + + private static final String KEY_STATE = "state"; + + private static final int STATE_INIT = 0; + private static final int STATE_LOADED = 1; + private static final int STATE_SAVING = 2; + + public static final String DOWNLOAD_STRING = "download"; + public static final File DOWNLOAD_BUCKET = new File( + Environment.getExternalStorageDirectory(), DOWNLOAD_STRING); + + public static final String CROP_ACTION = "com.android.camera.action.CROP"; + + private int mState = STATE_INIT; + + private CropView mCropView; + + private boolean mDoFaceDetection = true; + + private Handler mMainHandler; + + // We keep the following members so that we can free them + + // mBitmap is the unrotated bitmap we pass in to mCropView for detect faces. + // mCropView is responsible for rotating it to the way that it is viewed by users. + private Bitmap mBitmap; + private BitmapTileProvider mBitmapTileProvider; + private BitmapRegionDecoder mRegionDecoder; + private Bitmap mBitmapInIntent; + private boolean mUseRegionDecoder = false; + + private ProgressDialog mProgressDialog; + private Future mLoadTask; + private Future mLoadBitmapTask; + private Future mSaveTask; + + private MediaItem mMediaItem; + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + requestWindowFeature(Window.FEATURE_ACTION_BAR); + requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + + // Initialize UI + setContentView(R.layout.cropimage); + mCropView = new CropView(this); + getGLRoot().setContentPane(mCropView); + + mMainHandler = new SynchronizedHandler(getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_LARGE_BITMAP: { + mProgressDialog.dismiss(); + onBitmapRegionDecoderAvailable((BitmapRegionDecoder) message.obj); + break; + } + case MSG_BITMAP: { + mProgressDialog.dismiss(); + onBitmapAvailable((Bitmap) message.obj); + break; + } + case MSG_SAVE_COMPLETE: { + mProgressDialog.dismiss(); + setResult(RESULT_OK, (Intent) message.obj); + finish(); + break; + } + } + } + }; + + setCropParameters(); + } + + @Override + protected void onSaveInstanceState(Bundle saveState) { + saveState.putInt(KEY_STATE, mState); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.crop, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.cancel: { + setResult(RESULT_CANCELED); + finish(); + break; + } + case R.id.save: { + onSaveClicked(); + break; + } + } + return true; + } + + private class SaveOutput implements Job { + private RectF mCropRect; + + public SaveOutput(RectF cropRect) { + mCropRect = cropRect; + } + + public Intent run(JobContext jc) { + RectF cropRect = mCropRect; + Bundle extra = getIntent().getExtras(); + + Rect rect = new Rect( + Math.round(cropRect.left), Math.round(cropRect.top), + Math.round(cropRect.right), Math.round(cropRect.bottom)); + + Intent result = new Intent(); + result.putExtra(KEY_CROPPED_RECT, rect); + Bitmap cropped = null; + boolean outputted = false; + if (extra != null) { + Uri uri = (Uri) extra.getParcelable(MediaStore.EXTRA_OUTPUT); + if (uri != null) { + if (jc.isCancelled()) return null; + outputted = true; + cropped = getCroppedImage(rect); + if (!saveBitmapToUri(jc, cropped, uri)) return null; + } + if (extra.getBoolean(KEY_RETURN_DATA, false)) { + if (jc.isCancelled()) return null; + outputted = true; + if (cropped == null) cropped = getCroppedImage(rect); + result.putExtra(KEY_DATA, cropped); + } + if (extra.getBoolean(KEY_SET_AS_WALLPAPER, false)) { + if (jc.isCancelled()) return null; + outputted = true; + if (cropped == null) cropped = getCroppedImage(rect); + if (!setAsWallpaper(jc, cropped)) return null; + } + } + if (!outputted) { + if (jc.isCancelled()) return null; + if (cropped == null) cropped = getCroppedImage(rect); + Uri data = saveToMediaProvider(jc, cropped); + if (data != null) result.setData(data); + } + return result; + } + } + + public static String determineCompressFormat(MediaObject obj) { + String compressFormat = "JPEG"; + if (obj instanceof MediaItem) { + String mime = ((MediaItem) obj).getMimeType(); + if (mime.contains("png") || mime.contains("gif")) { + // Set the compress format to PNG for png and gif images + // because they may contain alpha values. + compressFormat = "PNG"; + } + } + return compressFormat; + } + + private boolean setAsWallpaper(JobContext jc, Bitmap wallpaper) { + try { + WallpaperManager.getInstance(this).setBitmap(wallpaper); + } catch (IOException e) { + Log.w(TAG, "fail to set wall paper", e); + } + return true; + } + + private File saveMedia( + JobContext jc, Bitmap cropped, File directory, String filename) { + // Try file-1.jpg, file-2.jpg, ... until we find a filename + // which does not exist yet. + File candidate = null; + String fileExtension = getFileExtension(); + for (int i = 1; i < MAX_FILE_INDEX; ++i) { + candidate = new File(directory, filename + "-" + i + "." + + fileExtension); + try { + if (candidate.createNewFile()) break; + } catch (IOException e) { + Log.e(TAG, "fail to create new file: " + + candidate.getAbsolutePath(), e); + return null; + } + } + if (!candidate.exists() || !candidate.isFile()) { + throw new RuntimeException("cannot create file: " + filename); + } + + candidate.setReadable(true, false); + candidate.setWritable(true, false); + + try { + FileOutputStream fos = new FileOutputStream(candidate); + try { + saveBitmapToOutputStream(jc, cropped, + convertExtensionToCompressFormat(fileExtension), fos); + } finally { + fos.close(); + } + } catch (IOException e) { + Log.e(TAG, "fail to save image: " + + candidate.getAbsolutePath(), e); + candidate.delete(); + return null; + } + + if (jc.isCancelled()) { + candidate.delete(); + return null; + } + + return candidate; + } + + private Uri saveToMediaProvider(JobContext jc, Bitmap cropped) { + if (PicasaSource.isPicasaImage(mMediaItem)) { + return savePicasaImage(jc, cropped); + } else if (mMediaItem instanceof LocalImage) { + return saveLocalImage(jc, cropped); + } else { + Log.w(TAG, "no output for crop image " + mMediaItem); + return null; + } + } + + private Uri savePicasaImage(JobContext jc, Bitmap cropped) { + if (!DOWNLOAD_BUCKET.isDirectory() && !DOWNLOAD_BUCKET.mkdirs()) { + throw new RuntimeException("cannot create download folder"); + } + + String filename = PicasaSource.getImageTitle(mMediaItem); + int pos = filename.lastIndexOf('.'); + if (pos >= 0) filename = filename.substring(0, pos); + File output = saveMedia(jc, cropped, DOWNLOAD_BUCKET, filename); + if (output == null) return null; + + long now = System.currentTimeMillis() / 1000; + ContentValues values = new ContentValues(); + values.put(Images.Media.TITLE, PicasaSource.getImageTitle(mMediaItem)); + values.put(Images.Media.DISPLAY_NAME, output.getName()); + values.put(Images.Media.DATE_TAKEN, PicasaSource.getDateTaken(mMediaItem)); + values.put(Images.Media.DATE_MODIFIED, now); + values.put(Images.Media.DATE_ADDED, now); + values.put(Images.Media.MIME_TYPE, "image/jpeg"); + values.put(Images.Media.ORIENTATION, 0); + values.put(Images.Media.DATA, output.getAbsolutePath()); + values.put(Images.Media.SIZE, output.length()); + + double latitude = PicasaSource.getLatitude(mMediaItem); + double longitude = PicasaSource.getLongitude(mMediaItem); + if (GalleryUtils.isValidLocation(latitude, longitude)) { + values.put(Images.Media.LATITUDE, latitude); + values.put(Images.Media.LONGITUDE, longitude); + } + return getContentResolver().insert( + Images.Media.EXTERNAL_CONTENT_URI, values); + } + + private Uri saveLocalImage(JobContext jc, Bitmap cropped) { + LocalImage localImage = (LocalImage) mMediaItem; + + File oldPath = new File(localImage.filePath); + File directory = new File(oldPath.getParent()); + + String filename = oldPath.getName(); + int pos = filename.lastIndexOf('.'); + if (pos >= 0) filename = filename.substring(0, pos); + File output = saveMedia(jc, cropped, directory, filename); + if (output == null) return null; + + long now = System.currentTimeMillis() / 1000; + ContentValues values = new ContentValues(); + values.put(Images.Media.TITLE, localImage.caption); + values.put(Images.Media.DISPLAY_NAME, output.getName()); + values.put(Images.Media.DATE_TAKEN, localImage.dateTakenInMs); + values.put(Images.Media.DATE_MODIFIED, now); + values.put(Images.Media.DATE_ADDED, now); + values.put(Images.Media.MIME_TYPE, "image/jpeg"); + values.put(Images.Media.ORIENTATION, 0); + values.put(Images.Media.DATA, output.getAbsolutePath()); + values.put(Images.Media.SIZE, output.length()); + + if (GalleryUtils.isValidLocation(localImage.latitude, localImage.longitude)) { + values.put(Images.Media.LATITUDE, localImage.latitude); + values.put(Images.Media.LONGITUDE, localImage.longitude); + } + return getContentResolver().insert( + Images.Media.EXTERNAL_CONTENT_URI, values); + } + + private boolean saveBitmapToOutputStream( + JobContext jc, Bitmap bitmap, CompressFormat format, OutputStream os) { + // We wrap the OutputStream so that it can be interrupted. + final InterruptableOutputStream ios = new InterruptableOutputStream(os); + jc.setCancelListener(new CancelListener() { + public void onCancel() { + ios.interrupt(); + } + }); + try { + bitmap.compress(format, DEFAULT_COMPRESS_QUALITY, os); + if (!jc.isCancelled()) return false; + } finally { + jc.setCancelListener(null); + Utils.closeSilently(os); + } + return false; + } + + private boolean saveBitmapToUri(JobContext jc, Bitmap bitmap, Uri uri) { + try { + return saveBitmapToOutputStream(jc, bitmap, + convertExtensionToCompressFormat(getFileExtension()), + getContentResolver().openOutputStream(uri)); + } catch (FileNotFoundException e) { + Log.w(TAG, "cannot write output", e); + } + return true; + } + + private CompressFormat convertExtensionToCompressFormat(String extension) { + return extension.equals("png") + ? CompressFormat.PNG + : CompressFormat.JPEG; + } + + private String getFileExtension() { + String requestFormat = getIntent().getStringExtra(KEY_OUTPUT_FORMAT); + String outputFormat = (requestFormat == null) + ? determineCompressFormat(mMediaItem) + : requestFormat; + + outputFormat = outputFormat.toLowerCase(); + return (outputFormat.equals("png") || outputFormat.equals("gif")) + ? "png" // We don't support gif compression. + : "jpg"; + } + + private void onSaveClicked() { + Bundle extra = getIntent().getExtras(); + RectF cropRect = mCropView.getCropRectangle(); + if (cropRect == null) return; + mState = STATE_SAVING; + int messageId = extra != null && extra.getBoolean(KEY_SET_AS_WALLPAPER) + ? R.string.wallpaper + : R.string.saving_image; + mProgressDialog = ProgressDialog.show( + this, null, getString(messageId), true, false); + mSaveTask = getThreadPool().submit(new SaveOutput(cropRect), + new FutureListener() { + public void onFutureDone(Future future) { + mSaveTask = null; + if (future.get() == null) return; + mMainHandler.sendMessage(mMainHandler.obtainMessage( + MSG_SAVE_COMPLETE, future.get())); + } + }); + } + + private Bitmap getCroppedImage(Rect rect) { + Utils.assertTrue(rect.width() > 0 && rect.height() > 0); + + Bundle extras = getIntent().getExtras(); + // (outputX, outputY) = the width and height of the returning bitmap. + int outputX = rect.width(); + int outputY = rect.height(); + if (extras != null) { + outputX = extras.getInt(KEY_OUTPUT_X, outputX); + outputY = extras.getInt(KEY_OUTPUT_Y, outputY); + } + + if (outputX * outputY > MAX_PIXEL_COUNT) { + float scale = (float) Math.sqrt( + (double) MAX_PIXEL_COUNT / outputX / outputY); + Log.w(TAG, "scale down the cropped image: " + scale); + outputX = Math.round(scale * outputX); + outputY = Math.round(scale * outputY); + } + + // (rect.width() * scaleX, rect.height() * scaleY) = + // the size of drawing area in output bitmap + float scaleX = 1; + float scaleY = 1; + Rect dest = new Rect(0, 0, outputX, outputY); + if (extras == null || extras.getBoolean(KEY_SCALE, true)) { + scaleX = (float) outputX / rect.width(); + scaleY = (float) outputY / rect.height(); + if (extras == null || !extras.getBoolean( + KEY_SCALE_UP_IF_NEEDED, false)) { + if (scaleX > 1f) scaleX = 1; + if (scaleY > 1f) scaleY = 1; + } + } + + // Keep the content in the center (or crop the content) + int rectWidth = Math.round(rect.width() * scaleX); + int rectHeight = Math.round(rect.height() * scaleY); + dest.set(Math.round((outputX - rectWidth) / 2f), + Math.round((outputY - rectHeight) / 2f), + Math.round((outputX + rectWidth) / 2f), + Math.round((outputY + rectHeight) / 2f)); + + if (mBitmapInIntent != null) { + Bitmap source = mBitmapInIntent; + Bitmap result = Bitmap.createBitmap( + outputX, outputY, Config.ARGB_8888); + Canvas canvas = new Canvas(result); + canvas.drawBitmap(source, rect, dest, null); + return result; + } + + int rotation = mMediaItem.getRotation(); + rotateRectangle(rect, mCropView.getImageWidth(), + mCropView.getImageHeight(), 360 - rotation); + rotateRectangle(dest, outputX, outputY, 360 - rotation); + if (mUseRegionDecoder) { + BitmapFactory.Options options = new BitmapFactory.Options(); + int sample = BitmapUtils.computeSampleSizeLarger( + Math.max(scaleX, scaleY)); + options.inSampleSize = sample; + if ((rect.width() / sample) == dest.width() + && (rect.height() / sample) == dest.height() + && rotation == 0) { + // To prevent concurrent access in GLThread + synchronized (mRegionDecoder) { + return mRegionDecoder.decodeRegion(rect, options); + } + } + Bitmap result = Bitmap.createBitmap( + outputX, outputY, Config.ARGB_8888); + Canvas canvas = new Canvas(result); + rotateCanvas(canvas, outputX, outputY, rotation); + drawInTiles(canvas, mRegionDecoder, rect, dest, sample); + return result; + } else { + Bitmap result = Bitmap.createBitmap(outputX, outputY, Config.ARGB_8888); + Canvas canvas = new Canvas(result); + rotateCanvas(canvas, outputX, outputY, rotation); + canvas.drawBitmap(mBitmap, + rect, dest, new Paint(Paint.FILTER_BITMAP_FLAG)); + return result; + } + } + + private static void rotateCanvas( + Canvas canvas, int width, int height, int rotation) { + canvas.translate(width / 2, height / 2); + canvas.rotate(rotation); + if (((rotation / 90) & 0x01) == 0) { + canvas.translate(-width / 2, -height / 2); + } else { + canvas.translate(-height / 2, -width / 2); + } + } + + private static void rotateRectangle( + Rect rect, int width, int height, int rotation) { + if (rotation == 0 || rotation == 360) return; + + int w = rect.width(); + int h = rect.height(); + switch (rotation) { + case 90: { + rect.top = rect.left; + rect.left = height - rect.bottom; + rect.right = rect.left + h; + rect.bottom = rect.top + w; + return; + } + case 180: { + rect.left = width - rect.right; + rect.top = height - rect.bottom; + rect.right = rect.left + w; + rect.bottom = rect.top + h; + return; + } + case 270: { + rect.left = rect.top; + rect.top = width - rect.right; + rect.right = rect.left + h; + rect.bottom = rect.top + w; + return; + } + default: throw new AssertionError(); + } + } + + private void drawInTiles(Canvas canvas, + BitmapRegionDecoder decoder, Rect rect, Rect dest, int sample) { + int tileSize = TILE_SIZE * sample; + Rect tileRect = new Rect(); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Config.ARGB_8888; + options.inSampleSize = sample; + canvas.translate(dest.left, dest.top); + canvas.scale((float) sample * dest.width() / rect.width(), + (float) sample * dest.height() / rect.height()); + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG); + for (int tx = rect.left, x = 0; + tx < rect.right; tx += tileSize, x += TILE_SIZE) { + for (int ty = rect.top, y = 0; + ty < rect.bottom; ty += tileSize, y += TILE_SIZE) { + tileRect.set(tx, ty, tx + tileSize, ty + tileSize); + if (tileRect.intersect(rect)) { + Bitmap bitmap; + + // To prevent concurrent access in GLThread + synchronized (decoder) { + bitmap = decoder.decodeRegion(tileRect, options); + } + canvas.drawBitmap(bitmap, x, y, paint); + bitmap.recycle(); + } + } + } + } + + private void onBitmapRegionDecoderAvailable( + BitmapRegionDecoder regionDecoder) { + + if (regionDecoder == null) { + Toast.makeText(this, "fail to load image", Toast.LENGTH_SHORT).show(); + finish(); + return; + } + mRegionDecoder = regionDecoder; + mUseRegionDecoder = true; + mState = STATE_LOADED; + + BitmapFactory.Options options = new BitmapFactory.Options(); + int width = regionDecoder.getWidth(); + int height = regionDecoder.getHeight(); + options.inSampleSize = BitmapUtils.computeSampleSize(width, height, + BitmapUtils.UNCONSTRAINED, BACKUP_PIXEL_COUNT); + mBitmap = regionDecoder.decodeRegion( + new Rect(0, 0, width, height), options); + mCropView.setDataModel(new TileImageViewAdapter( + mBitmap, regionDecoder), mMediaItem.getRotation()); + if (mDoFaceDetection) { + mCropView.detectFaces(mBitmap); + } else { + mCropView.initializeHighlightRectangle(); + } + } + + private void onBitmapAvailable(Bitmap bitmap) { + if (bitmap == null) { + Toast.makeText(this, "fail to load image", Toast.LENGTH_SHORT).show(); + finish(); + return; + } + mUseRegionDecoder = false; + mState = STATE_LOADED; + + mBitmap = bitmap; + BitmapFactory.Options options = new BitmapFactory.Options(); + mCropView.setDataModel(new BitmapTileProvider(bitmap, 512), + mMediaItem.getRotation()); + if (mDoFaceDetection) { + mCropView.detectFaces(bitmap); + } else { + mCropView.initializeHighlightRectangle(); + } + } + + private void setCropParameters() { + Bundle extras = getIntent().getExtras(); + if (extras == null) + return; + int aspectX = extras.getInt(KEY_ASPECT_X, 0); + int aspectY = extras.getInt(KEY_ASPECT_Y, 0); + if (aspectX != 0 && aspectY != 0) { + mCropView.setAspectRatio((float) aspectX / aspectY); + } + + float spotlightX = extras.getFloat(KEY_SPOTLIGHT_X, 0); + float spotlightY = extras.getFloat(KEY_SPOTLIGHT_Y, 0); + if (spotlightX != 0 && spotlightY != 0) { + mCropView.setSpotlightRatio(spotlightX, spotlightY); + } + } + + private void initializeData() { + Bundle extras = getIntent().getExtras(); + + if (extras != null) { + if (extras.containsKey(KEY_NO_FACE_DETECTION)) { + mDoFaceDetection = !extras.getBoolean(KEY_NO_FACE_DETECTION); + } + + mBitmapInIntent = extras.getParcelable(KEY_DATA); + + if (mBitmapInIntent != null) { + mBitmapTileProvider = + new BitmapTileProvider(mBitmapInIntent, MAX_BACKUP_IMAGE_SIZE); + mCropView.setDataModel(mBitmapTileProvider, 0); + if (mDoFaceDetection) { + mCropView.detectFaces(mBitmapInIntent); + } else { + mCropView.initializeHighlightRectangle(); + } + mState = STATE_LOADED; + return; + } + } + + mProgressDialog = ProgressDialog.show( + this, null, getString(R.string.loading_image), true, false); + + mMediaItem = getMediaItemFromIntentData(); + if (mMediaItem == null) return; + + boolean supportedByBitmapRegionDecoder = + (mMediaItem.getSupportedOperations() & MediaItem.SUPPORT_FULL_IMAGE) != 0; + if (supportedByBitmapRegionDecoder) { + mLoadTask = getThreadPool().submit(new LoadDataTask(mMediaItem), + new FutureListener() { + public void onFutureDone(Future future) { + mLoadTask = null; + BitmapRegionDecoder decoder = future.get(); + if (future.isCancelled()) { + if (decoder != null) decoder.recycle(); + return; + } + mMainHandler.sendMessage(mMainHandler.obtainMessage( + MSG_LARGE_BITMAP, decoder)); + } + }); + } else { + mLoadBitmapTask = getThreadPool().submit(new LoadBitmapDataTask(mMediaItem), + new FutureListener() { + public void onFutureDone(Future future) { + mLoadBitmapTask = null; + Bitmap bitmap = future.get(); + if (future.isCancelled()) { + if (bitmap != null) bitmap.recycle(); + return; + } + mMainHandler.sendMessage(mMainHandler.obtainMessage( + MSG_BITMAP, bitmap)); + } + }); + } + } + + @Override + protected void onResume() { + super.onResume(); + if (mState == STATE_INIT) initializeData(); + if (mState == STATE_SAVING) onSaveClicked(); + + // TODO: consider to do it in GLView system + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + mCropView.resume(); + } finally { + root.unlockRenderThread(); + } + } + + @Override + protected void onPause() { + super.onPause(); + + Future loadTask = mLoadTask; + if (loadTask != null && !loadTask.isDone()) { + // load in progress, try to cancel it + loadTask.cancel(); + loadTask.waitDone(); + mProgressDialog.dismiss(); + } + + Future loadBitmapTask = mLoadBitmapTask; + if (loadBitmapTask != null && !loadBitmapTask.isDone()) { + // load in progress, try to cancel it + loadBitmapTask.cancel(); + loadBitmapTask.waitDone(); + mProgressDialog.dismiss(); + } + + Future saveTask = mSaveTask; + if (saveTask != null && !saveTask.isDone()) { + // save in progress, try to cancel it + saveTask.cancel(); + saveTask.waitDone(); + mProgressDialog.dismiss(); + } + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + mCropView.pause(); + } finally { + root.unlockRenderThread(); + } + } + + private MediaItem getMediaItemFromIntentData() { + Uri uri = getIntent().getData(); + DataManager manager = getDataManager(); + if (uri == null) { + Log.w(TAG, "no data given"); + return null; + } + Path path = manager.findPathByUri(uri); + if (path == null) { + Log.w(TAG, "cannot get path for: " + uri); + return null; + } + return (MediaItem) manager.getMediaObject(path); + } + + private class LoadDataTask implements Job { + MediaItem mItem; + + public LoadDataTask(MediaItem item) { + mItem = item; + } + + public BitmapRegionDecoder run(JobContext jc) { + return mItem == null ? null : mItem.requestLargeImage().run(jc); + } + } + + private class LoadBitmapDataTask implements Job { + MediaItem mItem; + + public LoadBitmapDataTask(MediaItem item) { + mItem = item; + } + public Bitmap run(JobContext jc) { + return mItem == null + ? null + : mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc); + } + } +} diff --git a/src/com/android/gallery3d/app/DialogPicker.java b/src/com/android/gallery3d/app/DialogPicker.java new file mode 100644 index 000000000..ebfc52158 --- /dev/null +++ b/src/com/android/gallery3d/app/DialogPicker.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.GLRootView; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; + +public class DialogPicker extends AbstractGalleryActivity + implements OnClickListener { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.dialog_picker); + ((GLRootView) findViewById(R.id.gl_root_view)).setZOrderOnTop(true); + findViewById(R.id.cancel).setOnClickListener(this); + + int typeBits = GalleryUtils.determineTypeBits(this, getIntent()); + setTitle(GalleryUtils.getSelectionModePrompt(typeBits)); + Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + Bundle data = extras == null ? new Bundle() : new Bundle(extras); + + data.putBoolean(Gallery.KEY_GET_CONTENT, true); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, + getDataManager().getTopSetPath(typeBits)); + getStateManager().startState(AlbumSetPage.class, data); + } + + @Override + public void onBackPressed() { + // send the back event to the top sub-state + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + getStateManager().getTopState().onBackPressed(); + } finally { + root.unlockRenderThread(); + } + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.cancel) finish(); + } +} diff --git a/src/com/android/gallery3d/app/EyePosition.java b/src/com/android/gallery3d/app/EyePosition.java new file mode 100644 index 000000000..1c3aa60bb --- /dev/null +++ b/src/com/android/gallery3d/app/EyePosition.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.SystemClock; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; + +public class EyePosition { + private static final String TAG = "EyePosition"; + + public interface EyePositionListener { + public void onEyePositionChanged(float x, float y, float z); + } + + private static final float GYROSCOPE_THRESHOLD = 0.15f; + private static final float GYROSCOPE_LIMIT = 10f; + private static final int GYROSCOPE_SETTLE_DOWN = 15; + private static final float GYROSCOPE_RESTORE_FACTOR = 0.995f; + + private static final double USER_ANGEL = Math.toRadians(10); + private static final float USER_ANGEL_COS = (float) Math.cos(USER_ANGEL); + private static final float USER_ANGEL_SIN = (float) Math.sin(USER_ANGEL); + private static final float MAX_VIEW_RANGE = (float) 0.5; + private static final int NOT_STARTED = -1; + + private static final float USER_DISTANCE_METER = 0.3f; + + private Context mContext; + private EyePositionListener mListener; + private Display mDisplay; + // The eyes' position of the user, the origin is at the center of the + // device and the unit is in pixels. + private float mX; + private float mY; + private float mZ; + + private final float mUserDistance; // in pixel + private final float mLimit; + private long mStartTime = NOT_STARTED; + private Sensor mSensor; + private PositionListener mPositionListener = new PositionListener(); + + private int mGyroscopeCountdown = 0; + + public EyePosition(Context context, EyePositionListener listener) { + mContext = context; + mListener = listener; + mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER); + mLimit = mUserDistance * MAX_VIEW_RANGE; + + WindowManager wManager = (WindowManager) mContext + .getSystemService(Context.WINDOW_SERVICE); + mDisplay = wManager.getDefaultDisplay(); + + SensorManager sManager = (SensorManager) mContext + .getSystemService(Context.SENSOR_SERVICE); + mSensor = sManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); + if (mSensor == null) { + Log.w(TAG, "no gyroscope, use accelerometer instead"); + mSensor = sManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + } + if (mSensor == null) { + Log.w(TAG, "no sensor available"); + } + } + + public void resetPosition() { + mStartTime = NOT_STARTED; + mX = mY = 0; + mZ = -mUserDistance; + mListener.onEyePositionChanged(mX, mY, mZ); + } + + /* + * We assume the user is at the following position + * + * /|\ user's eye + * | / + * -G(gravity) | / + * |_/ + * / |/_____\ -Y (-y direction of device) + * user angel + */ + private void onAccelerometerChanged(float gx, float gy, float gz) { + + float x = gx, y = gy, z = gz; + + switch (mDisplay.getRotation()) { + case Surface.ROTATION_90: x = -gy; y= gx; break; + case Surface.ROTATION_180: x = -gx; y = -gy; break; + case Surface.ROTATION_270: x = gy; y = -gx; break; + } + + float temp = x * x + y * y + z * z; + float t = -y /temp; + + float tx = t * x; + float ty = -1 + t * y; + float tz = t * z; + + float length = (float) Math.sqrt(tx * tx + ty * ty + tz * tz); + float glength = (float) Math.sqrt(temp); + + mX = Utils.clamp((x * USER_ANGEL_COS / glength + + tx * USER_ANGEL_SIN / length) * mUserDistance, + -mLimit, mLimit); + mY = -Utils.clamp((y * USER_ANGEL_COS / glength + + ty * USER_ANGEL_SIN / length) * mUserDistance, + -mLimit, mLimit); + mZ = (float) -Math.sqrt( + mUserDistance * mUserDistance - mX * mX - mY * mY); + mListener.onEyePositionChanged(mX, mY, mZ); + } + + private void onGyroscopeChanged(float gx, float gy, float gz) { + long now = SystemClock.elapsedRealtime(); + float distance = (gx > 0 ? gx : -gx) + (gy > 0 ? gy : - gy); + if (distance < GYROSCOPE_THRESHOLD + || distance > GYROSCOPE_LIMIT || mGyroscopeCountdown > 0) { + --mGyroscopeCountdown; + mStartTime = now; + float limit = mUserDistance / 20f; + if (mX > limit || mX < -limit || mY > limit || mY < -limit) { + mX *= GYROSCOPE_RESTORE_FACTOR; + mY *= GYROSCOPE_RESTORE_FACTOR; + mZ = (float) -Math.sqrt( + mUserDistance * mUserDistance - mX * mX - mY * mY); + mListener.onEyePositionChanged(mX, mY, mZ); + } + return; + } + + float t = (now - mStartTime) / 1000f * mUserDistance * (-mZ); + mStartTime = now; + + float x = -gy, y = -gx; + switch (mDisplay.getRotation()) { + case Surface.ROTATION_90: x = -gx; y= gy; break; + case Surface.ROTATION_180: x = gy; y = gx; break; + case Surface.ROTATION_270: x = gx; y = -gy; break; + } + + mX = Utils.clamp((float) (mX + x * t / Math.hypot(mZ, mX)), + -mLimit, mLimit) * GYROSCOPE_RESTORE_FACTOR; + mY = Utils.clamp((float) (mY + y * t / Math.hypot(mZ, mY)), + -mLimit, mLimit) * GYROSCOPE_RESTORE_FACTOR; + + mZ = (float) -Math.sqrt( + mUserDistance * mUserDistance - mX * mX - mY * mY); + mListener.onEyePositionChanged(mX, mY, mZ); + } + + private class PositionListener implements SensorEventListener { + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + public void onSensorChanged(SensorEvent event) { + switch (event.sensor.getType()) { + case Sensor.TYPE_GYROSCOPE: { + onGyroscopeChanged( + event.values[0], event.values[1], event.values[2]); + break; + } + case Sensor.TYPE_ACCELEROMETER: { + onAccelerometerChanged( + event.values[0], event.values[1], event.values[2]); + } + } + } + } + + public void pause() { + if (mSensor != null) { + SensorManager sManager = (SensorManager) mContext + .getSystemService(Context.SENSOR_SERVICE); + sManager.unregisterListener(mPositionListener); + } + } + + public void resume() { + if (mSensor != null) { + SensorManager sManager = (SensorManager) mContext + .getSystemService(Context.SENSOR_SERVICE); + sManager.registerListener(mPositionListener, + mSensor, SensorManager.SENSOR_DELAY_GAME); + } + + mStartTime = NOT_STARTED; + mGyroscopeCountdown = GYROSCOPE_SETTLE_DOWN; + mX = mY = 0; + mZ = -mUserDistance; + mListener.onEyePositionChanged(mX, mY, mZ); + } +} diff --git a/src/com/android/gallery3d/app/FilterUtils.java b/src/com/android/gallery3d/app/FilterUtils.java new file mode 100644 index 000000000..9b8ea2d62 --- /dev/null +++ b/src/com/android/gallery3d/app/FilterUtils.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; + +// This class handles filtering and clustering. +// +// We allow at most only one filter operation at a time (Currently it +// doesn't make sense to use more than one). Also each clustering operation +// can be applied at most once. In addition, there is one more constraint +// ("fixed set constraint") described below. +// +// A clustered album (not including album set) and its base sets are fixed. +// For example, +// +// /cluster/{base_set}/time/7 +// +// This set and all sets inside base_set (recursively) are fixed because +// 1. We can not change this set to use another clustering condition (like +// changing "time" to "location"). +// 2. Neither can we change any set in the base_set. +// The reason is in both cases the 7th set may not exist in the new clustering. +// --------------------- +// newPath operation: create a new path based on a source path and put an extra +// condition on top of it: +// +// T = newFilterPath(S, filterType); +// T = newClusterPath(S, clusterType); +// +// Similar functions can be used to replace the current condition (if there is one). +// +// T = switchFilterPath(S, filterType); +// T = switchClusterPath(S, clusterType); +// +// For all fixed set in the path defined above, if some clusterType and +// filterType are already used, they cannot not be used as parameter for these +// functions. setupMenuItems() makes sure those types cannot be selected. +// +public class FilterUtils { + private static final String TAG = "FilterUtils"; + + public static final int CLUSTER_BY_ALBUM = 1; + public static final int CLUSTER_BY_TIME = 2; + public static final int CLUSTER_BY_LOCATION = 4; + public static final int CLUSTER_BY_TAG = 8; + public static final int CLUSTER_BY_SIZE = 16; + public static final int CLUSTER_BY_FACE = 32; + + public static final int FILTER_IMAGE_ONLY = 1; + public static final int FILTER_VIDEO_ONLY = 2; + public static final int FILTER_ALL = 4; + + // These are indices of the return values of getAppliedFilters(). + // The _F suffix means "fixed". + private static final int CLUSTER_TYPE = 0; + private static final int FILTER_TYPE = 1; + private static final int CLUSTER_TYPE_F = 2; + private static final int FILTER_TYPE_F = 3; + private static final int CLUSTER_CURRENT_TYPE = 4; + private static final int FILTER_CURRENT_TYPE = 5; + + public static void setupMenuItems(GalleryActionBar model, Path path, boolean inAlbum) { + int[] result = new int[6]; + getAppliedFilters(path, result); + int ctype = result[CLUSTER_TYPE]; + int ftype = result[FILTER_TYPE]; + int ftypef = result[FILTER_TYPE_F]; + int ccurrent = result[CLUSTER_CURRENT_TYPE]; + int fcurrent = result[FILTER_CURRENT_TYPE]; + + setMenuItemApplied(model, CLUSTER_BY_TIME, + (ctype & CLUSTER_BY_TIME) != 0, (ccurrent & CLUSTER_BY_TIME) != 0); + setMenuItemApplied(model, CLUSTER_BY_LOCATION, + (ctype & CLUSTER_BY_LOCATION) != 0, (ccurrent & CLUSTER_BY_LOCATION) != 0); + setMenuItemApplied(model, CLUSTER_BY_TAG, + (ctype & CLUSTER_BY_TAG) != 0, (ccurrent & CLUSTER_BY_TAG) != 0); + setMenuItemApplied(model, CLUSTER_BY_FACE, + (ctype & CLUSTER_BY_FACE) != 0, (ccurrent & CLUSTER_BY_FACE) != 0); + + model.setClusterItemVisibility(CLUSTER_BY_ALBUM, !inAlbum || ctype == 0); + + setMenuItemApplied(model, R.id.action_cluster_album, ctype == 0, + ccurrent == 0); + + // A filtering is available if it's not applied, and the old filtering + // (if any) is not fixed. + setMenuItemAppliedEnabled(model, R.string.show_images_only, + (ftype & FILTER_IMAGE_ONLY) != 0, + (ftype & FILTER_IMAGE_ONLY) == 0 && ftypef == 0, + (fcurrent & FILTER_IMAGE_ONLY) != 0); + setMenuItemAppliedEnabled(model, R.string.show_videos_only, + (ftype & FILTER_VIDEO_ONLY) != 0, + (ftype & FILTER_VIDEO_ONLY) == 0 && ftypef == 0, + (fcurrent & FILTER_VIDEO_ONLY) != 0); + setMenuItemAppliedEnabled(model, R.string.show_all, + ftype == 0, ftype != 0 && ftypef == 0, fcurrent == 0); + } + + // Gets the filters applied in the path. + private static void getAppliedFilters(Path path, int[] result) { + getAppliedFilters(path, result, false); + } + + private static void getAppliedFilters(Path path, int[] result, boolean underCluster) { + String[] segments = path.split(); + // Recurse into sub media sets. + for (int i = 0; i < segments.length; i++) { + if (segments[i].startsWith("{")) { + String[] sets = Path.splitSequence(segments[i]); + for (int j = 0; j < sets.length; j++) { + Path sub = Path.fromString(sets[j]); + getAppliedFilters(sub, result, underCluster); + } + } + } + + // update current selection + if (segments[0].equals("cluster")) { + // if this is a clustered album, set underCluster to true. + if (segments.length == 4) { + underCluster = true; + } + + int ctype = toClusterType(segments[2]); + result[CLUSTER_TYPE] |= ctype; + result[CLUSTER_CURRENT_TYPE] = ctype; + if (underCluster) { + result[CLUSTER_TYPE_F] |= ctype; + } + } + } + + private static int toClusterType(String s) { + if (s.equals("time")) { + return CLUSTER_BY_TIME; + } else if (s.equals("location")) { + return CLUSTER_BY_LOCATION; + } else if (s.equals("tag")) { + return CLUSTER_BY_TAG; + } else if (s.equals("size")) { + return CLUSTER_BY_SIZE; + } else if (s.equals("face")) { + return CLUSTER_BY_FACE; + } + return 0; + } + + private static void setMenuItemApplied( + GalleryActionBar model, int id, boolean applied, boolean updateTitle) { + model.setClusterItemEnabled(id, !applied); + } + + private static void setMenuItemAppliedEnabled(GalleryActionBar model, int id, boolean applied, boolean enabled, boolean updateTitle) { + model.setClusterItemEnabled(id, enabled); + } + + // Add a specified filter to the path. + public static String newFilterPath(String base, int filterType) { + int mediaType; + switch (filterType) { + case FILTER_IMAGE_ONLY: + mediaType = MediaObject.MEDIA_TYPE_IMAGE; + break; + case FILTER_VIDEO_ONLY: + mediaType = MediaObject.MEDIA_TYPE_VIDEO; + break; + default: /* FILTER_ALL */ + return base; + } + + return "/filter/mediatype/" + mediaType + "/{" + base + "}"; + } + + // Add a specified clustering to the path. + public static String newClusterPath(String base, int clusterType) { + String kind; + switch (clusterType) { + case CLUSTER_BY_TIME: + kind = "time"; + break; + case CLUSTER_BY_LOCATION: + kind = "location"; + break; + case CLUSTER_BY_TAG: + kind = "tag"; + break; + case CLUSTER_BY_SIZE: + kind = "size"; + break; + case CLUSTER_BY_FACE: + kind = "face"; + break; + default: /* CLUSTER_BY_ALBUM */ + return base; + } + + return "/cluster/{" + base + "}/" + kind; + } + + // Change the topmost filter to the specified type. + public static String switchFilterPath(String base, int filterType) { + return newFilterPath(removeOneFilterFromPath(base), filterType); + } + + // Change the topmost clustering to the specified type. + public static String switchClusterPath(String base, int clusterType) { + return newClusterPath(removeOneClusterFromPath(base), clusterType); + } + + // Remove the topmost clustering (if any) from the path. + private static String removeOneClusterFromPath(String base) { + boolean[] done = new boolean[1]; + return removeOneClusterFromPath(base, done); + } + + private static String removeOneClusterFromPath(String base, boolean[] done) { + if (done[0]) return base; + + String[] segments = Path.split(base); + if (segments[0].equals("cluster")) { + done[0] = true; + return Path.splitSequence(segments[1])[0]; + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < segments.length; i++) { + sb.append("/"); + if (segments[i].startsWith("{")) { + sb.append("{"); + String[] sets = Path.splitSequence(segments[i]); + for (int j = 0; j < sets.length; j++) { + if (j > 0) { + sb.append(","); + } + sb.append(removeOneClusterFromPath(sets[j], done)); + } + sb.append("}"); + } else { + sb.append(segments[i]); + } + } + return sb.toString(); + } + + // Remove the topmost filter (if any) from the path. + private static String removeOneFilterFromPath(String base) { + boolean[] done = new boolean[1]; + return removeOneFilterFromPath(base, done); + } + + private static String removeOneFilterFromPath(String base, boolean[] done) { + if (done[0]) return base; + + String[] segments = Path.split(base); + if (segments[0].equals("filter") && segments[1].equals("mediatype")) { + done[0] = true; + return Path.splitSequence(segments[3])[0]; + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < segments.length; i++) { + sb.append("/"); + if (segments[i].startsWith("{")) { + sb.append("{"); + String[] sets = Path.splitSequence(segments[i]); + for (int j = 0; j < sets.length; j++) { + if (j > 0) { + sb.append(","); + } + sb.append(removeOneFilterFromPath(sets[j], done)); + } + sb.append("}"); + } else { + sb.append(segments[i]); + } + } + return sb.toString(); + } +} diff --git a/src/com/android/gallery3d/app/Gallery.java b/src/com/android/gallery3d/app/Gallery.java new file mode 100644 index 000000000..2c5263b03 --- /dev/null +++ b/src/com/android/gallery3d/app/Gallery.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.util.GalleryUtils; + +import android.app.ActionBar; +import android.content.ContentResolver; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.Window; +import android.widget.Toast; + +public final class Gallery extends AbstractGalleryActivity { + public static final String EXTRA_SLIDESHOW = "slideshow"; + public static final String EXTRA_CROP = "crop"; + + public static final String ACTION_REVIEW = "com.android.camera.action.REVIEW"; + public static final String KEY_GET_CONTENT = "get-content"; + public static final String KEY_GET_ALBUM = "get-album"; + public static final String KEY_TYPE_BITS = "type-bits"; + public static final String KEY_MEDIA_TYPES = "mediaTypes"; + + private static final String TAG = "Gallery"; + private GalleryActionBar mActionBar; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_ACTION_BAR); + requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + + setContentView(R.layout.main); + mActionBar = new GalleryActionBar(this); + + if (savedInstanceState != null) { + getStateManager().restoreFromState(savedInstanceState); + } else { + initializeByIntent(); + } + } + + private void initializeByIntent() { + Intent intent = getIntent(); + String action = intent.getAction(); + + if (Intent.ACTION_GET_CONTENT.equalsIgnoreCase(action)) { + startGetContent(intent); + } else if (Intent.ACTION_PICK.equalsIgnoreCase(action)) { + // We do NOT really support the PICK intent. Handle it as + // the GET_CONTENT. However, we need to translate the type + // in the intent here. + Log.w(TAG, "action PICK is not supported"); + String type = Utils.ensureNotNull(intent.getType()); + if (type.startsWith("vnd.android.cursor.dir/")) { + if (type.endsWith("/image")) intent.setType("image/*"); + if (type.endsWith("/video")) intent.setType("video/*"); + } + startGetContent(intent); + } else if (Intent.ACTION_VIEW.equalsIgnoreCase(action) + || ACTION_REVIEW.equalsIgnoreCase(action)){ + startViewAction(intent); + } else { + startDefaultPage(); + } + } + + public void startDefaultPage() { + Bundle data = new Bundle(); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, + getDataManager().getTopSetPath(DataManager.INCLUDE_ALL)); + getStateManager().startState(AlbumSetPage.class, data); + } + + private void startGetContent(Intent intent) { + Bundle data = intent.getExtras() != null + ? new Bundle(intent.getExtras()) + : new Bundle(); + data.putBoolean(KEY_GET_CONTENT, true); + int typeBits = GalleryUtils.determineTypeBits(this, intent); + data.putInt(KEY_TYPE_BITS, typeBits); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, + getDataManager().getTopSetPath(typeBits)); + getStateManager().startState(AlbumSetPage.class, data); + } + + private String getContentType(Intent intent) { + String type = intent.getType(); + if (type != null) return type; + + Uri uri = intent.getData(); + try { + return getContentResolver().getType(uri); + } catch (Throwable t) { + Log.w(TAG, "get type fail", t); + return null; + } + } + + private void startViewAction(Intent intent) { + Boolean slideshow = intent.getBooleanExtra(EXTRA_SLIDESHOW, false); + if (slideshow) { + getActionBar().hide(); + DataManager manager = getDataManager(); + Path path = manager.findPathByUri(intent.getData()); + if (path == null || manager.getMediaObject(path) + instanceof MediaItem) { + path = Path.fromString( + manager.getTopSetPath(DataManager.INCLUDE_IMAGE)); + } + Bundle data = new Bundle(); + data.putString(SlideshowPage.KEY_SET_PATH, path.toString()); + data.putBoolean(SlideshowPage.KEY_RANDOM_ORDER, true); + data.putBoolean(SlideshowPage.KEY_REPEAT, true); + getStateManager().startState(SlideshowPage.class, data); + } else { + Bundle data = new Bundle(); + DataManager dm = getDataManager(); + Uri uri = intent.getData(); + String contentType = getContentType(intent); + if (contentType == null) { + Toast.makeText(this, + R.string.no_such_item, Toast.LENGTH_LONG).show(); + finish(); + return; + } + if (contentType.startsWith( + ContentResolver.CURSOR_DIR_BASE_TYPE)) { + int mediaType = intent.getIntExtra(KEY_MEDIA_TYPES, 0); + if (mediaType != 0) { + uri = uri.buildUpon().appendQueryParameter( + KEY_MEDIA_TYPES, String.valueOf(mediaType)) + .build(); + } + Path albumPath = dm.findPathByUri(uri); + if (albumPath != null) { + MediaSet mediaSet = (MediaSet) dm.getMediaObject(albumPath); + data.putString(AlbumPage.KEY_MEDIA_PATH, albumPath.toString()); + getStateManager().startState(AlbumPage.class, data); + } else { + startDefaultPage(); + } + } else { + Path itemPath = dm.findPathByUri(uri); + Path albumPath = dm.getDefaultSetOf(itemPath); + if (albumPath != null) { + data.putString(PhotoPage.KEY_MEDIA_SET_PATH, + albumPath.toString()); + } + data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, itemPath.toString()); + getStateManager().startState(PhotoPage.class, data); + } + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + return getStateManager().createOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + return getStateManager().itemSelected(item); + } finally { + root.unlockRenderThread(); + } + } + + @Override + public void onBackPressed() { + // send the back event to the top sub-state + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + getStateManager().onBackPressed(); + } finally { + root.unlockRenderThread(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + getStateManager().destroy(); + } finally { + root.unlockRenderThread(); + } + } + + @Override + protected void onResume() { + Utils.assertTrue(getStateManager().getStateCount() > 0); + super.onResume(); + } + + @Override + public GalleryActionBar getGalleryActionBar() { + return mActionBar; + } +} diff --git a/src/com/android/gallery3d/app/GalleryActionBar.java b/src/com/android/gallery3d/app/GalleryActionBar.java new file mode 100644 index 000000000..b9b59ee39 --- /dev/null +++ b/src/com/android/gallery3d/app/GalleryActionBar.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import java.util.ArrayList; + +import com.android.gallery3d.R; + +import android.app.ActionBar; +import android.app.ActionBar.Tab; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.FragmentTransaction; +import android.content.Context; +import android.content.DialogInterface; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ShareActionProvider; + +public class GalleryActionBar implements ActionBar.TabListener { + private static final String TAG = "GalleryActionBar"; + + public interface ClusterRunner { + public void doCluster(int id); + } + + private static class ActionItem { + public int action; + public boolean enabled; + public boolean visible; + public int tabTitle; + public int dialogTitle; + public int clusterBy; + + public ActionItem(int action, boolean applied, boolean enabled, int title, + int clusterBy) { + this(action, applied, enabled, title, title, clusterBy); + } + + public ActionItem(int action, boolean applied, boolean enabled, int tabTitle, + int dialogTitle, int clusterBy) { + this.action = action; + this.enabled = enabled; + this.tabTitle = tabTitle; + this.dialogTitle = dialogTitle; + this.clusterBy = clusterBy; + this.visible = true; + } + } + + private static final ActionItem[] sClusterItems = new ActionItem[] { + new ActionItem(FilterUtils.CLUSTER_BY_ALBUM, true, false, R.string.albums, + R.string.group_by_album), + new ActionItem(FilterUtils.CLUSTER_BY_LOCATION, true, false, + R.string.locations, R.string.location, R.string.group_by_location), + new ActionItem(FilterUtils.CLUSTER_BY_TIME, true, false, R.string.times, + R.string.time, R.string.group_by_time), + new ActionItem(FilterUtils.CLUSTER_BY_FACE, true, false, R.string.people, + R.string.group_by_faces), + new ActionItem(FilterUtils.CLUSTER_BY_TAG, true, false, R.string.tags, + R.string.group_by_tags) + }; + + private ClusterRunner mClusterRunner; + private CharSequence[] mTitles; + private ArrayList mActions; + private Context mContext; + private ActionBar mActionBar; + // We need this because ActionBar.getSelectedTab() doesn't work when + // ActionBar is hidden. + private Tab mCurrentTab; + + public GalleryActionBar(Activity activity) { + mActionBar = activity.getActionBar(); + mContext = activity; + + for (ActionItem item : sClusterItems) { + mActionBar.addTab(mActionBar.newTab().setText(item.tabTitle). + setTag(item).setTabListener(this)); + } + } + + public static int getHeight(Activity activity) { + ActionBar actionBar = activity.getActionBar(); + return actionBar != null ? actionBar.getHeight() : 0; + } + + private void createDialogData() { + ArrayList titles = new ArrayList(); + mActions = new ArrayList(); + for (ActionItem item : sClusterItems) { + if (item.enabled && item.visible) { + titles.add(mContext.getString(item.dialogTitle)); + mActions.add(item.action); + } + } + mTitles = new CharSequence[titles.size()]; + titles.toArray(mTitles); + } + + public void setClusterItemEnabled(int id, boolean enabled) { + for (ActionItem item : sClusterItems) { + if (item.action == id) { + item.enabled = enabled; + return; + } + } + } + + public void setClusterItemVisibility(int id, boolean visible) { + for (ActionItem item : sClusterItems) { + if (item.action == id) { + item.visible = visible; + return; + } + } + } + + public int getClusterTypeAction() { + if (mCurrentTab != null) { + ActionItem item = (ActionItem) mCurrentTab.getTag(); + return item.action; + } + // By default, it's group-by-album + return FilterUtils.CLUSTER_BY_ALBUM; + } + + public static String getClusterByTypeString(Context context, int type) { + for (ActionItem item : sClusterItems) { + if (item.action == type) { + return context.getString(item.clusterBy); + } + } + return null; + } + + public static ShareActionProvider initializeShareActionProvider(Menu menu) { + MenuItem item = menu.findItem(R.id.action_share); + ShareActionProvider shareActionProvider = null; + if (item != null) { + shareActionProvider = (ShareActionProvider) item.getActionProvider(); + shareActionProvider.setShareHistoryFileName( + ShareActionProvider.DEFAULT_SHARE_HISTORY_FILE_NAME); + } + return shareActionProvider; + } + + public void showClusterTabs(ClusterRunner runner) { + mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + mClusterRunner = runner; + } + + public void hideClusterTabs() { + mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); + mClusterRunner = null; + } + + public void showClusterDialog(final ClusterRunner clusterRunner) { + createDialogData(); + final ArrayList actions = mActions; + new AlertDialog.Builder(mContext).setTitle(R.string.group_by).setItems( + mTitles, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + clusterRunner.doCluster(actions.get(which).intValue()); + } + }).create().show(); + } + + public void setTitle(String title) { + if (mActionBar != null) mActionBar.setTitle(title); + } + + public void setTitle(int titleId) { + if (mActionBar != null) mActionBar.setTitle(titleId); + } + + public void setSubtitle(String title) { + if (mActionBar != null) mActionBar.setSubtitle(title); + } + + public void setNavigationMode(int mode) { + if (mActionBar != null) mActionBar.setNavigationMode(mode); + } + + public int getHeight() { + return mActionBar == null ? 0 : mActionBar.getHeight(); + } + + @Override + public void onTabSelected(Tab tab, FragmentTransaction ft) { + if (mCurrentTab == tab) return; + mCurrentTab = tab; + ActionItem item = (ActionItem) tab.getTag(); + if (mClusterRunner != null) mClusterRunner.doCluster(item.action); + } + + @Override + public void onTabUnselected(Tab tab, FragmentTransaction ft) { + } + + @Override + public void onTabReselected(Tab tab, FragmentTransaction ft) { + } +} diff --git a/src/com/android/gallery3d/app/GalleryActivity.java b/src/com/android/gallery3d/app/GalleryActivity.java new file mode 100644 index 000000000..02f2f72f3 --- /dev/null +++ b/src/com/android/gallery3d/app/GalleryActivity.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.PositionRepository; + +public interface GalleryActivity extends GalleryContext { + public StateManager getStateManager(); + public GLRoot getGLRoot(); + public PositionRepository getPositionRepository(); + public GalleryApp getGalleryApplication(); + public GalleryActionBar getGalleryActionBar(); +} diff --git a/src/com/android/gallery3d/app/GalleryApp.java b/src/com/android/gallery3d/app/GalleryApp.java new file mode 100644 index 000000000..b3a305e53 --- /dev/null +++ b/src/com/android/gallery3d/app/GalleryApp.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.DownloadCache; +import com.android.gallery3d.data.ImageCacheService; +import com.android.gallery3d.util.ThreadPool; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.os.Looper; + +public interface GalleryApp { + public DataManager getDataManager(); + public ImageCacheService getImageCacheService(); + public DownloadCache getDownloadCache(); + public ThreadPool getThreadPool(); + + public Context getAndroidContext(); + public Looper getMainLooper(); + public ContentResolver getContentResolver(); + public Resources getResources(); +} diff --git a/src/com/android/gallery3d/app/GalleryAppImpl.java b/src/com/android/gallery3d/app/GalleryAppImpl.java new file mode 100644 index 000000000..a11d92017 --- /dev/null +++ b/src/com/android/gallery3d/app/GalleryAppImpl.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.DownloadCache; +import com.android.gallery3d.data.ImageCacheService; +import com.android.gallery3d.picasasource.PicasaSource; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.widget.WidgetUtils; + +import android.app.Application; +import android.content.Context; + +import java.io.File; + +public class GalleryAppImpl extends Application implements GalleryApp { + + private static final String DOWNLOAD_FOLDER = "download"; + private static final long DOWNLOAD_CAPACITY = 64 * 1024 * 1024; // 64M + + private ImageCacheService mImageCacheService; + private DataManager mDataManager; + private ThreadPool mThreadPool; + private DownloadCache mDownloadCache; + + @Override + public void onCreate() { + super.onCreate(); + GalleryUtils.initialize(this); + WidgetUtils.initialize(this); + PicasaSource.initialize(this); + } + + public Context getAndroidContext() { + return this; + } + + public synchronized DataManager getDataManager() { + if (mDataManager == null) { + mDataManager = new DataManager(this); + mDataManager.initializeSourceMap(); + } + return mDataManager; + } + + public synchronized ImageCacheService getImageCacheService() { + if (mImageCacheService == null) { + mImageCacheService = new ImageCacheService(getAndroidContext()); + } + return mImageCacheService; + } + + public synchronized ThreadPool getThreadPool() { + if (mThreadPool == null) { + mThreadPool = new ThreadPool(); + } + return mThreadPool; + } + + public synchronized DownloadCache getDownloadCache() { + if (mDownloadCache == null) { + File cacheDir = new File(getExternalCacheDir(), DOWNLOAD_FOLDER); + + if (!cacheDir.isDirectory()) cacheDir.mkdirs(); + + if (!cacheDir.isDirectory()) { + throw new RuntimeException( + "fail to create: " + cacheDir.getAbsolutePath()); + } + mDownloadCache = new DownloadCache(this, cacheDir, DOWNLOAD_CAPACITY); + } + return mDownloadCache; + } +} diff --git a/src/com/android/gallery3d/app/GalleryContext.java b/src/com/android/gallery3d/app/GalleryContext.java new file mode 100644 index 000000000..022b4a704 --- /dev/null +++ b/src/com/android/gallery3d/app/GalleryContext.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.ImageCacheService; +import com.android.gallery3d.util.ThreadPool; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.os.Looper; + +public interface GalleryContext { + public ImageCacheService getImageCacheService(); + public DataManager getDataManager(); + + public Context getAndroidContext(); + + public Looper getMainLooper(); + public Resources getResources(); + public ContentResolver getContentResolver(); + public ThreadPool getThreadPool(); +} diff --git a/src/com/android/gallery3d/app/LoadingListener.java b/src/com/android/gallery3d/app/LoadingListener.java new file mode 100644 index 000000000..ecbd798d2 --- /dev/null +++ b/src/com/android/gallery3d/app/LoadingListener.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +public interface LoadingListener { + public void onLoadingStarted(); + public void onLoadingFinished(); +} diff --git a/src/com/android/gallery3d/app/Log.java b/src/com/android/gallery3d/app/Log.java new file mode 100644 index 000000000..07a8ea588 --- /dev/null +++ b/src/com/android/gallery3d/app/Log.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +public class Log { + public static int v(String tag, String msg) { + return android.util.Log.v(tag, msg); + } + public static int v(String tag, String msg, Throwable tr) { + return android.util.Log.v(tag, msg, tr); + } + public static int d(String tag, String msg) { + return android.util.Log.d(tag, msg); + } + public static int d(String tag, String msg, Throwable tr) { + return android.util.Log.d(tag, msg, tr); + } + public static int i(String tag, String msg) { + return android.util.Log.i(tag, msg); + } + public static int i(String tag, String msg, Throwable tr) { + return android.util.Log.i(tag, msg, tr); + } + public static int w(String tag, String msg) { + return android.util.Log.w(tag, msg); + } + public static int w(String tag, String msg, Throwable tr) { + return android.util.Log.w(tag, msg, tr); + } + public static int w(String tag, Throwable tr) { + return android.util.Log.w(tag, tr); + } + public static int e(String tag, String msg) { + return android.util.Log.e(tag, msg); + } + public static int e(String tag, String msg, Throwable tr) { + return android.util.Log.e(tag, msg, tr); + } +} diff --git a/src/com/android/gallery3d/app/ManageCachePage.java b/src/com/android/gallery3d/app/ManageCachePage.java new file mode 100644 index 000000000..a0190db77 --- /dev/null +++ b/src/com/android/gallery3d/app/ManageCachePage.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.AlbumSetView; +import com.android.gallery3d.ui.CacheBarView; +import com.android.gallery3d.ui.GLCanvas; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.ManageCacheDrawer; +import com.android.gallery3d.ui.MenuExecutor; +import com.android.gallery3d.ui.SelectionDrawer; +import com.android.gallery3d.ui.SelectionManager; +import com.android.gallery3d.ui.SlotView; +import com.android.gallery3d.ui.StaticBackground; +import com.android.gallery3d.util.GalleryUtils; + +import android.app.ActionBar; +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.widget.Toast; + +import java.util.ArrayList; + +public class ManageCachePage extends ActivityState implements + SelectionManager.SelectionListener, CacheBarView.Listener, + MenuExecutor.ProgressListener, EyePosition.EyePositionListener { + public static final String KEY_MEDIA_PATH = "media-path"; + private static final String TAG = "ManageCachePage"; + + private static final float USER_DISTANCE_METER = 0.3f; + private static final int DATA_CACHE_SIZE = 256; + + private StaticBackground mStaticBackground; + private AlbumSetView mAlbumSetView; + + private MediaSet mMediaSet; + + protected SelectionManager mSelectionManager; + protected SelectionDrawer mSelectionDrawer; + private AlbumSetDataAdapter mAlbumSetDataAdapter; + private float mUserDistance; // in pixel + + private CacheBarView mCacheBar; + + private EyePosition mEyePosition; + + // The eyes' position of the user, the origin is at the center of the + // device and the unit is in pixels. + private float mX; + private float mY; + private float mZ; + + private int mAlbumCountToMakeAvailableOffline; + + private GLView mRootPane = new GLView() { + private float mMatrix[] = new float[16]; + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + mStaticBackground.layout(0, 0, right - left, bottom - top); + mEyePosition.resetPosition(); + + Config.ManageCachePage config = Config.ManageCachePage.get((Context) mActivity); + + ActionBar actionBar = ((Activity) mActivity).getActionBar(); + int slotViewTop = GalleryActionBar.getHeight((Activity) mActivity); + int slotViewBottom = bottom - top - config.cacheBarHeight; + + mAlbumSetView.layout(0, slotViewTop, right - left, slotViewBottom); + mCacheBar.layout(0, bottom - top - config.cacheBarHeight, + right - left, bottom - top); + } + + @Override + protected void render(GLCanvas canvas) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + GalleryUtils.setViewPointMatrix(mMatrix, + getWidth() / 2 + mX, getHeight() / 2 + mY, mZ); + canvas.multiplyMatrix(mMatrix, 0); + super.render(canvas); + canvas.restore(); + } + }; + + public void onEyePositionChanged(float x, float y, float z) { + mRootPane.lockRendering(); + mX = x; + mY = y; + mZ = z; + mRootPane.unlockRendering(); + mRootPane.invalidate(); + } + + public void onSingleTapUp(int slotIndex) { + MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex); + if (targetSet == null) return; // Content is dirty, we shall reload soon + + // ignore selection action if the target set does not support cache + // operation (like a local album). + if ((targetSet.getSupportedOperations() + & MediaSet.SUPPORT_CACHE) == 0) { + showToastForLocalAlbum(); + return; + } + + Path path = targetSet.getPath(); + boolean isFullyCached = + (targetSet.getCacheFlag() == MediaObject.CACHE_FLAG_FULL); + boolean isSelected = mSelectionManager.isItemSelected(path); + + if (!isFullyCached) { + // We only count the media sets that will be made available offline + // in this session. + if (isSelected) { + --mAlbumCountToMakeAvailableOffline; + } else { + ++mAlbumCountToMakeAvailableOffline; + } + } + + long sizeOfTarget = targetSet.getCacheSize(); + if (isFullyCached ^ isSelected) { + mCacheBar.increaseTargetCacheSize(-sizeOfTarget); + } else { + mCacheBar.increaseTargetCacheSize(sizeOfTarget); + } + + mSelectionManager.toggle(path); + mAlbumSetView.invalidate(); + } + + @Override + public void onCreate(Bundle data, Bundle restoreState) { + initializeViews(); + initializeData(data); + mEyePosition = new EyePosition(mActivity.getAndroidContext(), this); + } + + @Override + public void onPause() { + super.onPause(); + mAlbumSetDataAdapter.pause(); + mAlbumSetView.pause(); + mCacheBar.pause(); + mEyePosition.pause(); + } + + @Override + public void onResume() { + super.onResume(); + setContentPane(mRootPane); + mAlbumSetDataAdapter.resume(); + mAlbumSetView.resume(); + mCacheBar.resume(); + mEyePosition.resume(); + } + + private void initializeData(Bundle data) { + mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER); + String mediaPath = data.getString(ManageCachePage.KEY_MEDIA_PATH); + mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath); + mSelectionManager.setSourceMediaSet(mMediaSet); + + // We will always be in selection mode in this page. + mSelectionManager.setAutoLeaveSelectionMode(false); + mSelectionManager.enterSelectionMode(); + + mAlbumSetDataAdapter = new AlbumSetDataAdapter( + mActivity, mMediaSet, DATA_CACHE_SIZE); + mAlbumSetView.setModel(mAlbumSetDataAdapter); + } + + private void initializeViews() { + mSelectionManager = new SelectionManager(mActivity, true); + mSelectionManager.setSelectionListener(this); + mStaticBackground = new StaticBackground(mActivity.getAndroidContext()); + mRootPane.addComponent(mStaticBackground); + + mSelectionDrawer = new ManageCacheDrawer( + (Context) mActivity, mSelectionManager); + Config.ManageCachePage config = Config.ManageCachePage.get((Context) mActivity); + mAlbumSetView = new AlbumSetView(mActivity, mSelectionDrawer, + config.slotWidth, config.slotHeight, + config.displayItemSize, config.labelFontSize, + config.labelOffsetY, config.labelMargin); + mAlbumSetView.setListener(new SlotView.SimpleListener() { + @Override + public void onSingleTapUp(int slotIndex) { + ManageCachePage.this.onSingleTapUp(slotIndex); + } + }); + mRootPane.addComponent(mAlbumSetView); + + mCacheBar = new CacheBarView(mActivity, R.drawable.manage_bar, + config.cacheBarHeight, + config.cacheBarPinLeftMargin, + config.cacheBarPinRightMargin, + config.cacheBarButtonRightMargin, + config.cacheBarFontSize); + + mCacheBar.setListener(this); + mRootPane.addComponent(mCacheBar); + + mStaticBackground.setImage(R.drawable.background, + R.drawable.background_portrait); + } + + public void onDoneClicked() { + ArrayList ids = mSelectionManager.getSelected(false); + if (ids.size() == 0) { + onBackPressed(); + return; + } + showToast(); + + MenuExecutor menuExecutor = new MenuExecutor(mActivity, + mSelectionManager); + menuExecutor.startAction(R.id.action_toggle_full_caching, + R.string.process_caching_requests, this); + } + + private void showToast() { + if (mAlbumCountToMakeAvailableOffline > 0) { + Activity activity = (Activity) mActivity; + Toast.makeText(activity, activity.getResources().getQuantityString( + R.plurals.make_albums_available_offline, + mAlbumCountToMakeAvailableOffline), + Toast.LENGTH_SHORT).show(); + } + } + + private void showToastForLocalAlbum() { + Activity activity = (Activity) mActivity; + Toast.makeText(activity, activity.getResources().getString( + R.string.try_to_set_local_album_available_offline), + Toast.LENGTH_SHORT).show(); + } + + public void onProgressComplete(int result) { + onBackPressed(); + } + + public void onProgressUpdate(int index) { + } + + public void onSelectionModeChange(int mode) { + } + + public void onSelectionChange(Path path, boolean selected) { + } +} diff --git a/src/com/android/gallery3d/app/MovieActivity.java b/src/com/android/gallery3d/app/MovieActivity.java new file mode 100644 index 000000000..fea364e85 --- /dev/null +++ b/src/com/android/gallery3d/app/MovieActivity.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.R; + +import android.app.Activity; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.database.Cursor; +import android.media.AudioManager; +import android.os.Bundle; +import android.provider.MediaStore; +import android.provider.MediaStore.Video.VideoColumns; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +/** + * This activity plays a video from a specified URI. + */ +public class MovieActivity extends Activity { + @SuppressWarnings("unused") + private static final String TAG = "MovieActivity"; + + private MoviePlayer mPlayer; + private boolean mFinishOnCompletion; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_ACTION_BAR); + requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + + setContentView(R.layout.movie_view); + View rootView = findViewById(R.id.root); + Intent intent = getIntent(); + setVideoTitle(intent); + mPlayer = new MoviePlayer(rootView, this, intent.getData()) { + @Override + public void onCompletion() { + if (mFinishOnCompletion) { + finish(); + } + } + }; + if (intent.hasExtra(MediaStore.EXTRA_SCREEN_ORIENTATION)) { + int orientation = intent.getIntExtra( + MediaStore.EXTRA_SCREEN_ORIENTATION, + ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + if (orientation != getRequestedOrientation()) { + setRequestedOrientation(orientation); + } + } + mFinishOnCompletion = intent.getBooleanExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, true); + Window win = getWindow(); + WindowManager.LayoutParams winParams = win.getAttributes(); + winParams.buttonBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF; + win.setAttributes(winParams); + + } + + private void setVideoTitle(Intent intent) { + String title = intent.getStringExtra(Intent.EXTRA_TITLE); + if (title == null) { + Cursor cursor = null; + try { + cursor = getContentResolver().query(intent.getData(), + new String[] {VideoColumns.TITLE}, null, null, null); + if (cursor != null && cursor.moveToNext()) { + title = cursor.getString(0); + } + } catch (Throwable t) { + Log.w(TAG, "cannot get title from: " + intent.getDataString(), t); + } finally { + if (cursor != null) cursor.close(); + } + } + if (title != null) getActionBar().setTitle(title); + } + + @Override + public void onStart() { + ((AudioManager) getSystemService(AUDIO_SERVICE)) + .requestAudioFocus(null, AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + super.onStart(); + } + + @Override + protected void onStop() { + ((AudioManager) getSystemService(AUDIO_SERVICE)) + .abandonAudioFocus(null); + super.onStop(); + } + + @Override + public void onPause() { + mPlayer.onPause(); + super.onPause(); + } + + @Override + public void onResume() { + mPlayer.onResume(); + super.onResume(); + } + + @Override + public void onDestroy() { + mPlayer.onDestroy(); + super.onDestroy(); + } +} diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java new file mode 100644 index 000000000..423994485 --- /dev/null +++ b/src/com/android/gallery3d/app/MoviePlayer.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.BlobCache; +import com.android.gallery3d.util.CacheManager; +import com.android.gallery3d.util.GalleryUtils; + +import android.app.ActionBar; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.Handler; +import android.view.KeyEvent; +import android.view.View; +import android.widget.MediaController; +import android.widget.VideoView; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +public class MoviePlayer implements + MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener { + @SuppressWarnings("unused") + private static final String TAG = "MoviePlayer"; + + // Copied from MediaPlaybackService in the Music Player app. + private static final String SERVICECMD = "com.android.music.musicservicecommand"; + private static final String CMDNAME = "command"; + private static final String CMDPAUSE = "pause"; + + private Context mContext; + private final VideoView mVideoView; + private final View mProgressView; + private final Bookmarker mBookmarker; + private final Uri mUri; + private final Handler mHandler = new Handler(); + private final AudioBecomingNoisyReceiver mAudioBecomingNoisyReceiver; + private final ActionBar mActionBar; + + private boolean mHasPaused; + + private final Runnable mPlayingChecker = new Runnable() { + public void run() { + if (mVideoView.isPlaying()) { + mProgressView.setVisibility(View.GONE); + } else { + mHandler.postDelayed(mPlayingChecker, 250); + } + } + }; + + public MoviePlayer(View rootView, final MovieActivity movieActivity, Uri videoUri) { + mContext = movieActivity.getApplicationContext(); + mVideoView = (VideoView) rootView.findViewById(R.id.surface_view); + mProgressView = rootView.findViewById(R.id.progress_indicator); + mBookmarker = new Bookmarker(movieActivity); + mActionBar = movieActivity.getActionBar(); + mUri = videoUri; + + // For streams that we expect to be slow to start up, show a + // progress spinner until playback starts. + String scheme = mUri.getScheme(); + if ("http".equalsIgnoreCase(scheme) || "rtsp".equalsIgnoreCase(scheme)) { + mHandler.postDelayed(mPlayingChecker, 250); + } else { + mProgressView.setVisibility(View.GONE); + } + + mVideoView.setOnErrorListener(this); + mVideoView.setOnCompletionListener(this); + mVideoView.setVideoURI(mUri); + + MediaController mediaController = new MediaController(movieActivity) { + @Override + public void show() { + super.show(); + mActionBar.show(); + } + + @Override + public void hide() { + super.hide(); + mActionBar.hide(); + } + }; + mVideoView.setMediaController(mediaController); + mediaController.setOnKeyListener(new View.OnKeyListener() { + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (event.getAction() == KeyEvent.ACTION_UP) { + movieActivity.onBackPressed(); + } + return true; + } + return false; + } + }); + + mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(); + mAudioBecomingNoisyReceiver.register(); + + // make the video view handle keys for seeking and pausing + mVideoView.requestFocus(); + + Intent i = new Intent(SERVICECMD); + i.putExtra(CMDNAME, CMDPAUSE); + movieActivity.sendBroadcast(i); + + final Integer bookmark = mBookmarker.getBookmark(mUri); + if (bookmark != null) { + showResumeDialog(movieActivity, bookmark); + } else { + mVideoView.start(); + } + } + + private void showResumeDialog(Context context, final int bookmark) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.resume_playing_title); + builder.setMessage(String.format( + context.getString(R.string.resume_playing_message), + GalleryUtils.formatDuration(context, bookmark / 1000))); + builder.setOnCancelListener(new OnCancelListener() { + public void onCancel(DialogInterface dialog) { + onCompletion(); + } + }); + builder.setPositiveButton( + R.string.resume_playing_resume, new OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + mVideoView.seekTo(bookmark); + mVideoView.start(); + } + }); + builder.setNegativeButton( + R.string.resume_playing_restart, new OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + mVideoView.start(); + } + }); + builder.show(); + } + + public void onPause() { + mHandler.removeCallbacksAndMessages(null); + mBookmarker.setBookmark(mUri, mVideoView.getCurrentPosition(), + mVideoView.getDuration()); + mVideoView.suspend(); + mHasPaused = true; + } + + public void onResume() { + if (mHasPaused) { + Integer bookmark = mBookmarker.getBookmark(mUri); + if (bookmark != null) { + mVideoView.seekTo(bookmark); + } + } + mVideoView.resume(); + } + + public void onDestroy() { + mVideoView.stopPlayback(); + mAudioBecomingNoisyReceiver.unregister(); + } + + public boolean onError(MediaPlayer player, int arg1, int arg2) { + mHandler.removeCallbacksAndMessages(null); + mProgressView.setVisibility(View.GONE); + return false; + } + + public void onCompletion(MediaPlayer mp) { + onCompletion(); + } + + public void onCompletion() { + } + + private class AudioBecomingNoisyReceiver extends BroadcastReceiver { + + public void register() { + mContext.registerReceiver(this, + new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + } + + public void unregister() { + mContext.unregisterReceiver(this); + } + + @Override + public void onReceive(Context context, Intent intent) { + if (mVideoView.isPlaying()) { + mVideoView.pause(); + } + } + } +} + +class Bookmarker { + private static final String TAG = "Bookmarker"; + + private static final String BOOKMARK_CACHE_FILE = "bookmark"; + private static final int BOOKMARK_CACHE_MAX_ENTRIES = 100; + private static final int BOOKMARK_CACHE_MAX_BYTES = 10 * 1024; + private static final int BOOKMARK_CACHE_VERSION = 1; + + private static final int HALF_MINUTE = 30 * 1000; + private static final int TWO_MINUTES = 4 * HALF_MINUTE; + + private final Context mContext; + + public Bookmarker(Context context) { + mContext = context; + } + + public void setBookmark(Uri uri, int bookmark, int duration) { + try { + BlobCache cache = CacheManager.getCache(mContext, + BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES, + BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeUTF(uri.toString()); + dos.writeInt(bookmark); + dos.writeInt(duration); + dos.flush(); + cache.insert(uri.hashCode(), bos.toByteArray()); + } catch (Throwable t) { + Log.w(TAG, "setBookmark failed", t); + } + } + + public Integer getBookmark(Uri uri) { + try { + BlobCache cache = CacheManager.getCache(mContext, + BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES, + BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION); + + byte[] data = cache.lookup(uri.hashCode()); + if (data == null) return null; + + DataInputStream dis = new DataInputStream( + new ByteArrayInputStream(data)); + + String uriString = dis.readUTF(dis); + int bookmark = dis.readInt(); + int duration = dis.readInt(); + + if (!uriString.equals(uri.toString())) { + return null; + } + + if ((bookmark < HALF_MINUTE) || (duration < TWO_MINUTES) + || (bookmark > (duration - HALF_MINUTE))) { + return null; + } + return Integer.valueOf(bookmark); + } catch (Throwable t) { + Log.w(TAG, "getBookmark failed", t); + } + return null; + } +} diff --git a/src/com/android/gallery3d/app/PackagesMonitor.java b/src/com/android/gallery3d/app/PackagesMonitor.java new file mode 100644 index 000000000..cb202a31c --- /dev/null +++ b/src/com/android/gallery3d/app/PackagesMonitor.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.picasasource.PicasaSource; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +public class PackagesMonitor extends BroadcastReceiver { + public static final String KEY_PACKAGES_VERSION = "packages-version"; + + public synchronized static int getPackagesVersion(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getInt(KEY_PACKAGES_VERSION, 1); + } + + @Override + public void onReceive(Context context, Intent intent) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + int version = prefs.getInt(KEY_PACKAGES_VERSION, 1); + prefs.edit().putInt(KEY_PACKAGES_VERSION, version + 1).commit(); + + String action = intent.getAction(); + String packageName = intent.getData().getSchemeSpecificPart(); + if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { + PicasaSource.onPackageAdded(context, packageName); + } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { + PicasaSource.onPackageRemoved(context, packageName); + } + } +} diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java new file mode 100644 index 000000000..c05c89a0d --- /dev/null +++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java @@ -0,0 +1,794 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.PhotoView; +import com.android.gallery3d.ui.PhotoView.ImageData; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.ui.TileImageViewAdapter; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool; + +import android.graphics.Bitmap; +import android.graphics.BitmapRegionDecoder; +import android.os.Handler; +import android.os.Message; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +public class PhotoDataAdapter implements PhotoPage.Model { + @SuppressWarnings("unused") + private static final String TAG = "PhotoDataAdapter"; + + private static final int MSG_LOAD_START = 1; + private static final int MSG_LOAD_FINISH = 2; + private static final int MSG_RUN_OBJECT = 3; + + private static final int MIN_LOAD_COUNT = 8; + private static final int DATA_CACHE_SIZE = 32; + private static final int IMAGE_CACHE_SIZE = 5; + + private static final int BIT_SCREEN_NAIL = 1; + private static final int BIT_FULL_IMAGE = 2; + + private static final long VERSION_OUT_OF_RANGE = MediaObject.nextVersionNumber(); + + // sImageFetchSeq is the fetching sequence for images. + // We want to fetch the current screennail first (offset = 0), the next + // screennail (offset = +1), then the previous screennail (offset = -1) etc. + // After all the screennail are fetched, we fetch the full images (only some + // of them because of we don't want to use too much memory). + private static ImageFetch[] sImageFetchSeq; + + private static class ImageFetch { + int indexOffset; + int imageBit; + public ImageFetch(int offset, int bit) { + indexOffset = offset; + imageBit = bit; + } + } + + static { + int k = 0; + sImageFetchSeq = new ImageFetch[1 + (IMAGE_CACHE_SIZE - 1) * 2 + 3]; + sImageFetchSeq[k++] = new ImageFetch(0, BIT_SCREEN_NAIL); + + for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) { + sImageFetchSeq[k++] = new ImageFetch(i, BIT_SCREEN_NAIL); + sImageFetchSeq[k++] = new ImageFetch(-i, BIT_SCREEN_NAIL); + } + + sImageFetchSeq[k++] = new ImageFetch(0, BIT_FULL_IMAGE); + sImageFetchSeq[k++] = new ImageFetch(1, BIT_FULL_IMAGE); + sImageFetchSeq[k++] = new ImageFetch(-1, BIT_FULL_IMAGE); + } + + private final TileImageViewAdapter mTileProvider = new TileImageViewAdapter(); + + // PhotoDataAdapter caches MediaItems (data) and ImageEntries (image). + // + // The MediaItems are stored in the mData array, which has DATA_CACHE_SIZE + // entries. The valid index range are [mContentStart, mContentEnd). We keep + // mContentEnd - mContentStart <= DATA_CACHE_SIZE, so we can use + // (i % DATA_CACHE_SIZE) as index to the array. + // + // The valid MediaItem window size (mContentEnd - mContentStart) may be + // smaller than DATA_CACHE_SIZE because we only update the window and reload + // the MediaItems when there are significant changes to the window position + // (>= MIN_LOAD_COUNT). + private final MediaItem mData[] = new MediaItem[DATA_CACHE_SIZE]; + private int mContentStart = 0; + private int mContentEnd = 0; + + /* + * The ImageCache is a version-to-ImageEntry map. It only holds + * the ImageEntries in the range of [mActiveStart, mActiveEnd). + * We also keep mActiveEnd - mActiveStart <= IMAGE_CACHE_SIZE. + * Besides, the [mActiveStart, mActiveEnd) range must be contained + * within the[mContentStart, mContentEnd) range. + */ + private HashMap mImageCache = new HashMap(); + private int mActiveStart = 0; + private int mActiveEnd = 0; + + // mCurrentIndex is the "center" image the user is viewing. The change of + // mCurrentIndex triggers the data loading and image loading. + private int mCurrentIndex; + + // mChanges keeps the version number (of MediaItem) about the previous, + // current, and next image. If the version number changes, we invalidate + // the model. This is used after a database reload or mCurrentIndex changes. + private final long mChanges[] = new long[3]; + + private final Handler mMainHandler; + private final ThreadPool mThreadPool; + + private final PhotoView mPhotoView; + private final MediaSet mSource; + private ReloadTask mReloadTask; + + private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; + private int mSize = 0; + private Path mItemPath; + private boolean mIsActive; + + public interface DataListener extends LoadingListener { + public void onPhotoChanged(int index, Path item); + } + + private DataListener mDataListener; + + private final SourceListener mSourceListener = new SourceListener(); + + // The path of the current viewing item will be stored in mItemPath. + // If mItemPath is not null, mCurrentIndex is only a hint for where we + // can find the item. If mItemPath is null, then we use the mCurrentIndex to + // find the image being viewed. + public PhotoDataAdapter(GalleryActivity activity, + PhotoView view, MediaSet mediaSet, Path itemPath, int indexHint) { + mSource = Utils.checkNotNull(mediaSet); + mPhotoView = Utils.checkNotNull(view); + mItemPath = Utils.checkNotNull(itemPath); + mCurrentIndex = indexHint; + mThreadPool = activity.getThreadPool(); + + Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION); + + mMainHandler = new SynchronizedHandler(activity.getGLRoot()) { + @SuppressWarnings("unchecked") + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_RUN_OBJECT: + ((Runnable) message.obj).run(); + return; + case MSG_LOAD_START: { + if (mDataListener != null) mDataListener.onLoadingStarted(); + return; + } + case MSG_LOAD_FINISH: { + if (mDataListener != null) mDataListener.onLoadingFinished(); + return; + } + default: throw new AssertionError(); + } + } + }; + + updateSlidingWindow(); + } + + private long getVersion(int index) { + if (index < 0 || index >= mSize) return VERSION_OUT_OF_RANGE; + if (index >= mContentStart && index < mContentEnd) { + MediaItem item = mData[index % DATA_CACHE_SIZE]; + if (item != null) return item.getDataVersion(); + } + return MediaObject.INVALID_DATA_VERSION; + } + + private void fireModelInvalidated() { + for (int i = -1; i <= 1; ++i) { + long current = getVersion(mCurrentIndex + i); + long change = mChanges[i + 1]; + if (current != change) { + mPhotoView.notifyImageInvalidated(i); + mChanges[i + 1] = current; + } + } + } + + public void setDataListener(DataListener listener) { + mDataListener = listener; + } + + private void updateScreenNail(long version, Future future) { + ImageEntry entry = mImageCache.get(version); + if (entry == null || entry.screenNailTask == null) { + Bitmap screenNail = future.get(); + if (screenNail != null) screenNail.recycle(); + return; + } + entry.screenNailTask = null; + entry.screenNail = future.get(); + + if (entry.screenNail == null) { + entry.failToLoad = true; + } else { + for (int i = -1; i <=1; ++i) { + if (version == getVersion(mCurrentIndex + i)) { + if (i == 0) updateTileProvider(entry); + mPhotoView.notifyImageInvalidated(i); + } + } + } + updateImageRequests(); + } + + private void updateFullImage(long version, Future future) { + ImageEntry entry = mImageCache.get(version); + if (entry == null || entry.fullImageTask == null) { + BitmapRegionDecoder fullImage = future.get(); + if (fullImage != null) fullImage.recycle(); + return; + } + entry.fullImageTask = null; + entry.fullImage = future.get(); + if (entry.fullImage != null) { + if (version == getVersion(mCurrentIndex)) { + updateTileProvider(entry); + mPhotoView.notifyImageInvalidated(0); + } + } + updateImageRequests(); + } + + public void resume() { + mIsActive = true; + mSource.addContentListener(mSourceListener); + updateImageCache(); + updateImageRequests(); + + mReloadTask = new ReloadTask(); + mReloadTask.start(); + + mPhotoView.notifyModelInvalidated(); + } + + public void pause() { + mIsActive = false; + + mReloadTask.terminate(); + mReloadTask = null; + + mSource.removeContentListener(mSourceListener); + + for (ImageEntry entry : mImageCache.values()) { + if (entry.fullImageTask != null) entry.fullImageTask.cancel(); + if (entry.screenNailTask != null) entry.screenNailTask.cancel(); + } + mImageCache.clear(); + mTileProvider.clear(); + } + + private ImageData getImage(int index) { + if (index < 0 || index >= mSize || !mIsActive) return null; + Utils.assertTrue(index >= mActiveStart && index < mActiveEnd); + + ImageEntry entry = mImageCache.get(getVersion(index)); + Bitmap screennail = entry == null ? null : entry.screenNail; + if (screennail != null) { + return new ImageData(screennail, entry.rotation); + } else { + return new ImageData(null, 0); + } + } + + public ImageData getPreviousImage() { + return getImage(mCurrentIndex - 1); + } + + public ImageData getNextImage() { + return getImage(mCurrentIndex + 1); + } + + private void updateCurrentIndex(int index) { + mCurrentIndex = index; + updateSlidingWindow(); + + MediaItem item = mData[index % DATA_CACHE_SIZE]; + mItemPath = item == null ? null : item.getPath(); + + updateImageCache(); + updateImageRequests(); + updateTileProvider(); + mPhotoView.notifyOnNewImage(); + + if (mDataListener != null) { + mDataListener.onPhotoChanged(index, mItemPath); + } + fireModelInvalidated(); + } + + public void next() { + updateCurrentIndex(mCurrentIndex + 1); + } + + public void previous() { + updateCurrentIndex(mCurrentIndex - 1); + } + + public void jumpTo(int index) { + if (mCurrentIndex == index) return; + updateCurrentIndex(index); + } + + public Bitmap getBackupImage() { + return mTileProvider.getBackupImage(); + } + + public int getImageHeight() { + return mTileProvider.getImageHeight(); + } + + public int getImageWidth() { + return mTileProvider.getImageWidth(); + } + + public int getImageRotation() { + ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex)); + return entry == null ? 0 : entry.rotation; + } + + public int getLevelCount() { + return mTileProvider.getLevelCount(); + } + + public Bitmap getTile(int level, int x, int y, int tileSize) { + return mTileProvider.getTile(level, x, y, tileSize); + } + + public boolean isFailedToLoad() { + return mTileProvider.isFailedToLoad(); + } + + public boolean isEmpty() { + return mSize == 0; + } + + public int getCurrentIndex() { + return mCurrentIndex; + } + + public MediaItem getCurrentMediaItem() { + return mData[mCurrentIndex % DATA_CACHE_SIZE]; + } + + public void setCurrentPhoto(Path path, int indexHint) { + if (mItemPath == path) return; + mItemPath = path; + mCurrentIndex = indexHint; + updateSlidingWindow(); + updateImageCache(); + fireModelInvalidated(); + + // We need to reload content if the path doesn't match. + MediaItem item = getCurrentMediaItem(); + if (item != null && item.getPath() != path) { + if (mReloadTask != null) mReloadTask.notifyDirty(); + } + } + + private void updateTileProvider() { + ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex)); + if (entry == null) { // in loading + mTileProvider.clear(); + } else { + updateTileProvider(entry); + } + } + + private void updateTileProvider(ImageEntry entry) { + Bitmap screenNail = entry.screenNail; + BitmapRegionDecoder fullImage = entry.fullImage; + if (screenNail != null) { + if (fullImage != null) { + mTileProvider.setBackupImage(screenNail, + fullImage.getWidth(), fullImage.getHeight()); + mTileProvider.setRegionDecoder(fullImage); + } else { + int width = screenNail.getWidth(); + int height = screenNail.getHeight(); + mTileProvider.setBackupImage(screenNail, width, height); + } + } else { + mTileProvider.clear(); + if (entry.failToLoad) mTileProvider.setFailedToLoad(); + } + } + + private void updateSlidingWindow() { + // 1. Update the image window + int start = Utils.clamp(mCurrentIndex - IMAGE_CACHE_SIZE / 2, + 0, Math.max(0, mSize - IMAGE_CACHE_SIZE)); + int end = Math.min(mSize, start + IMAGE_CACHE_SIZE); + + if (mActiveStart == start && mActiveEnd == end) return; + + mActiveStart = start; + mActiveEnd = end; + + // 2. Update the data window + start = Utils.clamp(mCurrentIndex - DATA_CACHE_SIZE / 2, + 0, Math.max(0, mSize - DATA_CACHE_SIZE)); + end = Math.min(mSize, start + DATA_CACHE_SIZE); + if (mContentStart > mActiveStart || mContentEnd < mActiveEnd + || Math.abs(start - mContentStart) > MIN_LOAD_COUNT) { + for (int i = mContentStart; i < mContentEnd; ++i) { + if (i < start || i >= end) { + mData[i % DATA_CACHE_SIZE] = null; + } + } + mContentStart = start; + mContentEnd = end; + if (mReloadTask != null) mReloadTask.notifyDirty(); + } + } + + private void updateImageRequests() { + if (!mIsActive) return; + + int currentIndex = mCurrentIndex; + MediaItem item = mData[currentIndex % DATA_CACHE_SIZE]; + if (item == null || item.getPath() != mItemPath) { + // current item mismatch - don't request image + return; + } + + // 1. Find the most wanted request and start it (if not already started). + Future task = null; + for (int i = 0; i < sImageFetchSeq.length; i++) { + int offset = sImageFetchSeq[i].indexOffset; + int bit = sImageFetchSeq[i].imageBit; + task = startTaskIfNeeded(currentIndex + offset, bit); + if (task != null) break; + } + + // 2. Cancel everything else. + for (ImageEntry entry : mImageCache.values()) { + if (entry.screenNailTask != null && entry.screenNailTask != task) { + entry.screenNailTask.cancel(); + entry.screenNailTask = null; + entry.requestedBits &= ~BIT_SCREEN_NAIL; + } + if (entry.fullImageTask != null && entry.fullImageTask != task) { + entry.fullImageTask.cancel(); + entry.fullImageTask = null; + entry.requestedBits &= ~BIT_FULL_IMAGE; + } + } + } + + // Returns the task if we started the task or the task is already started. + private Future startTaskIfNeeded(int index, int which) { + if (index < mActiveStart || index >= mActiveEnd) return null; + + ImageEntry entry = mImageCache.get(getVersion(index)); + if (entry == null) return null; + + if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null) { + return entry.screenNailTask; + } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null) { + return entry.fullImageTask; + } + + MediaItem item = mData[index % DATA_CACHE_SIZE]; + Utils.assertTrue(item != null); + + if (which == BIT_SCREEN_NAIL + && (entry.requestedBits & BIT_SCREEN_NAIL) == 0) { + entry.requestedBits |= BIT_SCREEN_NAIL; + entry.screenNailTask = mThreadPool.submit( + item.requestImage(MediaItem.TYPE_THUMBNAIL), + new ScreenNailListener(item.getDataVersion())); + // request screen nail + return entry.screenNailTask; + } + if (which == BIT_FULL_IMAGE + && (entry.requestedBits & BIT_FULL_IMAGE) == 0 + && (item.getSupportedOperations() + & MediaItem.SUPPORT_FULL_IMAGE) != 0) { + entry.requestedBits |= BIT_FULL_IMAGE; + entry.fullImageTask = mThreadPool.submit( + item.requestLargeImage(), + new FullImageListener(item.getDataVersion())); + // request full image + return entry.fullImageTask; + } + return null; + } + + private void updateImageCache() { + HashSet toBeRemoved = new HashSet(mImageCache.keySet()); + for (int i = mActiveStart; i < mActiveEnd; ++i) { + MediaItem item = mData[i % DATA_CACHE_SIZE]; + long version = item == null + ? MediaObject.INVALID_DATA_VERSION + : item.getDataVersion(); + if (version == MediaObject.INVALID_DATA_VERSION) continue; + ImageEntry entry = mImageCache.get(version); + toBeRemoved.remove(version); + if (entry != null) { + if (Math.abs(i - mCurrentIndex) > 1) { + if (entry.fullImageTask != null) { + entry.fullImageTask.cancel(); + entry.fullImageTask = null; + } + entry.fullImage = null; + entry.requestedBits &= ~BIT_FULL_IMAGE; + } + } else { + entry = new ImageEntry(); + entry.rotation = item.getRotation(); + mImageCache.put(version, entry); + } + } + + // Clear the data and requests for ImageEntries outside the new window. + for (Long version : toBeRemoved) { + ImageEntry entry = mImageCache.remove(version); + if (entry.fullImageTask != null) entry.fullImageTask.cancel(); + if (entry.screenNailTask != null) entry.screenNailTask.cancel(); + } + } + + private class FullImageListener + implements Runnable, FutureListener { + private final long mVersion; + private Future mFuture; + + public FullImageListener(long version) { + mVersion = version; + } + + public void onFutureDone(Future future) { + mFuture = future; + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); + } + + public void run() { + updateFullImage(mVersion, mFuture); + } + } + + private class ScreenNailListener + implements Runnable, FutureListener { + private final long mVersion; + private Future mFuture; + + public ScreenNailListener(long version) { + mVersion = version; + } + + public void onFutureDone(Future future) { + mFuture = future; + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); + } + + public void run() { + updateScreenNail(mVersion, mFuture); + } + } + + private static class ImageEntry { + public int requestedBits = 0; + public int rotation; + public BitmapRegionDecoder fullImage; + public Bitmap screenNail; + public Future screenNailTask; + public Future fullImageTask; + public boolean failToLoad = false; + } + + private class SourceListener implements ContentListener { + public void onContentDirty() { + if (mReloadTask != null) mReloadTask.notifyDirty(); + } + } + + private T executeAndWait(Callable callable) { + FutureTask task = new FutureTask(callable); + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); + try { + return task.get(); + } catch (InterruptedException e) { + return null; + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + private static class UpdateInfo { + public long version; + public boolean reloadContent; + public Path target; + public int indexHint; + public int contentStart; + public int contentEnd; + + public int size; + public ArrayList items; + } + + private class GetUpdateInfo implements Callable { + + private boolean needContentReload() { + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + if (mData[i % DATA_CACHE_SIZE] == null) return true; + } + MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; + return current == null || current.getPath() != mItemPath; + } + + @Override + public UpdateInfo call() throws Exception { + UpdateInfo info = new UpdateInfo(); + info.version = mSourceVersion; + info.reloadContent = needContentReload(); + info.target = mItemPath; + info.indexHint = mCurrentIndex; + info.contentStart = mContentStart; + info.contentEnd = mContentEnd; + info.size = mSize; + return info; + } + } + + private class UpdateContent implements Callable { + UpdateInfo mUpdateInfo; + + public UpdateContent(UpdateInfo updateInfo) { + mUpdateInfo = updateInfo; + } + + @Override + public Void call() throws Exception { + UpdateInfo info = mUpdateInfo; + mSourceVersion = info.version; + + if (info.size != mSize) { + mSize = info.size; + if (mContentEnd > mSize) mContentEnd = mSize; + if (mActiveEnd > mSize) mActiveEnd = mSize; + } + + if (info.indexHint == MediaSet.INDEX_NOT_FOUND) { + // The image has been deleted, clear mItemPath, the + // mCurrentIndex will be updated in the updateCurrentItem(). + mItemPath = null; + updateCurrentItem(); + } else { + mCurrentIndex = info.indexHint; + } + + updateSlidingWindow(); + + if (info.items != null) { + int start = Math.max(info.contentStart, mContentStart); + int end = Math.min(info.contentStart + info.items.size(), mContentEnd); + int dataIndex = start % DATA_CACHE_SIZE; + for (int i = start; i < end; ++i) { + mData[dataIndex] = info.items.get(i - info.contentStart); + if (++dataIndex == DATA_CACHE_SIZE) dataIndex = 0; + } + } + if (mItemPath == null) { + MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; + mItemPath = current == null ? null : current.getPath(); + } + updateImageCache(); + updateTileProvider(); + updateImageRequests(); + fireModelInvalidated(); + return null; + } + + private void updateCurrentItem() { + if (mSize == 0) return; + if (mCurrentIndex >= mSize) { + mCurrentIndex = mSize - 1; + mPhotoView.notifyOnNewImage(); + mPhotoView.startSlideInAnimation(PhotoView.TRANS_SLIDE_IN_LEFT); + } else { + mPhotoView.notifyOnNewImage(); + mPhotoView.startSlideInAnimation(PhotoView.TRANS_SLIDE_IN_RIGHT); + } + } + } + + private class ReloadTask extends Thread { + private volatile boolean mActive = true; + private volatile boolean mDirty = true; + + private boolean mIsLoading = false; + + private void updateLoading(boolean loading) { + if (mIsLoading == loading) return; + mIsLoading = loading; + mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); + } + + @Override + public void run() { + while (mActive) { + synchronized (this) { + if (!mDirty && mActive) { + updateLoading(false); + Utils.waitWithoutInterrupt(this); + continue; + } + } + mDirty = false; + UpdateInfo info = executeAndWait(new GetUpdateInfo()); + synchronized (DataManager.LOCK) { + updateLoading(true); + long version = mSource.reload(); + if (info.version != version) { + info.reloadContent = true; + info.size = mSource.getMediaItemCount(); + } + if (!info.reloadContent) continue; + info.items = mSource.getMediaItem(info.contentStart, info.contentEnd); + MediaItem item = findCurrentMediaItem(info); + if (item == null || item.getPath() != info.target) { + info.indexHint = findIndexOfTarget(info); + } + } + executeAndWait(new UpdateContent(info)); + } + } + + public synchronized void notifyDirty() { + mDirty = true; + notifyAll(); + } + + public synchronized void terminate() { + mActive = false; + notifyAll(); + } + + private MediaItem findCurrentMediaItem(UpdateInfo info) { + ArrayList items = info.items; + int index = info.indexHint - info.contentStart; + return index < 0 || index >= items.size() ? null : items.get(index); + } + + private int findIndexOfTarget(UpdateInfo info) { + if (info.target == null) return info.indexHint; + ArrayList items = info.items; + + // First, try to find the item in the data just loaded + if (items != null) { + for (int i = 0, n = items.size(); i < n; ++i) { + if (items.get(i).getPath() == info.target) return i + info.contentStart; + } + } + + // Not found, find it in mSource. + return mSource.getIndexOfItem(info.target, info.indexHint); + } + } +} diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java new file mode 100644 index 000000000..f28eb221d --- /dev/null +++ b/src/com/android/gallery3d/app/PhotoPage.java @@ -0,0 +1,581 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.MtpDevice; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.picasasource.PicasaSource; +import com.android.gallery3d.ui.DetailsWindow; +import com.android.gallery3d.ui.DetailsWindow.CloseListener; +import com.android.gallery3d.ui.DetailsWindow.DetailsSource; +import com.android.gallery3d.ui.FilmStripView; +import com.android.gallery3d.ui.GLCanvas; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.ImportCompleteListener; +import com.android.gallery3d.ui.MenuExecutor; +import com.android.gallery3d.ui.PhotoView; +import com.android.gallery3d.ui.PositionRepository; +import com.android.gallery3d.ui.PositionRepository.Position; +import com.android.gallery3d.ui.SelectionManager; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.ui.UserInteractionListener; +import com.android.gallery3d.util.GalleryUtils; + +import android.app.ActionBar; +import android.app.ActionBar.OnMenuVisibilityListener; +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.WindowManager; +import android.widget.ShareActionProvider; +import android.widget.Toast; + +public class PhotoPage extends ActivityState + implements PhotoView.PhotoTapListener, FilmStripView.Listener, + UserInteractionListener { + private static final String TAG = "PhotoPage"; + + private static final int MSG_HIDE_BARS = 1; + private static final int HIDE_BARS_TIMEOUT = 3500; + + private static final int REQUEST_SLIDESHOW = 1; + private static final int REQUEST_CROP = 2; + private static final int REQUEST_CROP_PICASA = 3; + + public static final String KEY_MEDIA_SET_PATH = "media-set-path"; + public static final String KEY_MEDIA_ITEM_PATH = "media-item-path"; + public static final String KEY_INDEX_HINT = "index-hint"; + + private GalleryApp mApplication; + private SelectionManager mSelectionManager; + + private PhotoView mPhotoView; + private PhotoPage.Model mModel; + private FilmStripView mFilmStripView; + private DetailsWindow mDetailsWindow; + private boolean mShowDetails; + + // mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied. + // E.g., viewing a photo in gmail attachment + private MediaSet mMediaSet; + private Menu mMenu; + + private Intent mResultIntent = new Intent(); + private int mCurrentIndex = 0; + private Handler mHandler; + private boolean mShowBars; + private ActionBar mActionBar; + private MyMenuVisibilityListener mMenuVisibilityListener; + private boolean mIsMenuVisible; + private boolean mIsInteracting; + private MediaItem mCurrentPhoto = null; + private MenuExecutor mMenuExecutor; + private boolean mIsActive; + private ShareActionProvider mShareActionProvider; + + public static interface Model extends PhotoView.Model { + public void resume(); + public void pause(); + public boolean isEmpty(); + public MediaItem getCurrentMediaItem(); + public int getCurrentIndex(); + public void setCurrentPhoto(Path path, int indexHint); + } + + private class MyMenuVisibilityListener implements OnMenuVisibilityListener { + public void onMenuVisibilityChanged(boolean isVisible) { + mIsMenuVisible = isVisible; + refreshHidingMessage(); + } + } + + private GLView mRootPane = new GLView() { + + @Override + protected void renderBackground(GLCanvas view) { + view.clearBuffer(); + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + mPhotoView.layout(0, 0, right - left, bottom - top); + PositionRepository.getInstance(mActivity).setOffset(0, 0); + int filmStripHeight = 0; + if (mFilmStripView != null) { + mFilmStripView.measure( + MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY), + MeasureSpec.UNSPECIFIED); + filmStripHeight = mFilmStripView.getMeasuredHeight(); + mFilmStripView.layout(0, bottom - top - filmStripHeight, + right - left, bottom - top); + } + if (mShowDetails) { + mDetailsWindow.measure( + MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int width = mDetailsWindow.getMeasuredWidth(); + int viewTop = GalleryActionBar.getHeight((Activity) mActivity); + mDetailsWindow.layout( + 0, viewTop, width, bottom - top - filmStripHeight); + } + } + }; + + @Override + public void onCreate(Bundle data, Bundle restoreState) { + mActionBar = ((Activity) mActivity).getActionBar(); + mSelectionManager = new SelectionManager(mActivity, false); + mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager); + + mPhotoView = new PhotoView(mActivity); + mPhotoView.setPhotoTapListener(this); + mRootPane.addComponent(mPhotoView); + mApplication = (GalleryApp)((Activity) mActivity).getApplication(); + + String setPathString = data.getString(KEY_MEDIA_SET_PATH); + Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH)); + + if (setPathString != null) { + mMediaSet = mActivity.getDataManager().getMediaSet(setPathString); + mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0); + mMediaSet = (MediaSet) + mActivity.getDataManager().getMediaObject(setPathString); + if (mMediaSet == null) { + Log.w(TAG, "failed to restore " + setPathString); + } + PhotoDataAdapter pda = new PhotoDataAdapter( + mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex); + mModel = pda; + mPhotoView.setModel(mModel); + + Config.PhotoPage config = Config.PhotoPage.get((Context) mActivity); + + mFilmStripView = new FilmStripView(mActivity, mMediaSet, + config.filmstripTopMargin, config.filmstripMidMargin, config.filmstripBottomMargin, + config.filmstripContentSize, config.filmstripThumbSize, config.filmstripBarSize, + config.filmstripGripSize, config.filmstripGripWidth); + mRootPane.addComponent(mFilmStripView); + mFilmStripView.setListener(this); + mFilmStripView.setUserInteractionListener(this); + mFilmStripView.setFocusIndex(mCurrentIndex); + mFilmStripView.setStartIndex(mCurrentIndex); + + mResultIntent.putExtra(KEY_INDEX_HINT, mCurrentIndex); + setStateResult(Activity.RESULT_OK, mResultIntent); + + pda.setDataListener(new PhotoDataAdapter.DataListener() { + + public void onPhotoChanged(int index, Path item) { + mFilmStripView.setFocusIndex(index); + mCurrentIndex = index; + mResultIntent.putExtra(KEY_INDEX_HINT, index); + if (item != null) { + mResultIntent.putExtra(KEY_MEDIA_ITEM_PATH, item.toString()); + MediaItem photo = mModel.getCurrentMediaItem(); + if (photo != null) updateCurrentPhoto(photo); + } else { + mResultIntent.removeExtra(KEY_MEDIA_ITEM_PATH); + } + setStateResult(Activity.RESULT_OK, mResultIntent); + } + + @Override + public void onLoadingFinished() { + GalleryUtils.setSpinnerVisibility((Activity) mActivity, false); + if (!mModel.isEmpty()) { + MediaItem photo = mModel.getCurrentMediaItem(); + if (photo != null) updateCurrentPhoto(photo); + } else if (mIsActive) { + mActivity.getStateManager().finishState(PhotoPage.this); + } + } + + + @Override + public void onLoadingStarted() { + GalleryUtils.setSpinnerVisibility((Activity) mActivity, true); + } + }); + } else { + // Get default media set by the URI + MediaItem mediaItem = (MediaItem) + mActivity.getDataManager().getMediaObject(itemPath); + mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem); + mPhotoView.setModel(mModel); + updateCurrentPhoto(mediaItem); + } + mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_HIDE_BARS: { + hideBars(); + break; + } + default: throw new AssertionError(message.what); + } + } + }; + + // start the opening animation + mPhotoView.setOpenedItem(itemPath); + } + + private void updateCurrentPhoto(MediaItem photo) { + if (mCurrentPhoto == photo) return; + mCurrentPhoto = photo; + if (mCurrentPhoto == null) return; + updateMenuOperations(); + if (mShowDetails) { + mDetailsWindow.reloadDetails(mModel.getCurrentIndex()); + } + String title = photo.getName(); + if (title != null) mActionBar.setTitle(title); + mPhotoView.showVideoPlayIcon(photo.getMediaType() + == MediaObject.MEDIA_TYPE_VIDEO); + + // If we have an ActionBar then we update the share intent + if (mShareActionProvider != null) { + Path path = photo.getPath(); + DataManager manager = mActivity.getDataManager(); + int type = manager.getMediaType(path); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType(MenuExecutor.getMimeType(type)); + intent.putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path)); + mShareActionProvider.setShareIntent(intent); + } + } + + private void updateMenuOperations() { + if (mCurrentPhoto == null || mMenu == null) return; + int supportedOperations = mCurrentPhoto.getSupportedOperations(); + if (!GalleryUtils.isEditorAvailable((Context) mActivity, "image/*")) { + supportedOperations &= ~MediaObject.SUPPORT_EDIT; + } + MenuExecutor.updateMenuOperation(mMenu, supportedOperations); + } + + private void showBars() { + if (mShowBars) return; + mShowBars = true; + mActionBar.show(); + WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); + params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE; + ((Activity) mActivity).getWindow().setAttributes(params); + if (mFilmStripView != null) { + mFilmStripView.show(); + } + } + + private void hideBars() { + if (!mShowBars) return; + mShowBars = false; + mActionBar.hide(); + WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); + params.systemUiVisibility = View. SYSTEM_UI_FLAG_LOW_PROFILE; + ((Activity) mActivity).getWindow().setAttributes(params); + if (mFilmStripView != null) { + mFilmStripView.hide(); + } + } + + private void refreshHidingMessage() { + mHandler.removeMessages(MSG_HIDE_BARS); + if (!mIsMenuVisible && !mIsInteracting) { + mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT); + } + } + + public void onUserInteraction() { + showBars(); + refreshHidingMessage(); + } + + public void onUserInteractionTap() { + if (mShowBars) { + hideBars(); + mHandler.removeMessages(MSG_HIDE_BARS); + } else { + showBars(); + refreshHidingMessage(); + } + } + + public void onUserInteractionBegin() { + showBars(); + mIsInteracting = true; + refreshHidingMessage(); + } + + public void onUserInteractionEnd() { + mIsInteracting = false; + refreshHidingMessage(); + } + + @Override + protected void onBackPressed() { + if (mShowDetails) { + hideDetails(); + } else { + PositionRepository repository = PositionRepository.getInstance(mActivity); + repository.clear(); + if (mCurrentPhoto != null) { + Position position = new Position(); + position.x = mRootPane.getWidth() / 2; + position.y = mRootPane.getHeight() / 2; + position.z = -1000; + repository.putPosition( + Long.valueOf(System.identityHashCode(mCurrentPhoto.getPath())), + position); + } + super.onBackPressed(); + } + } + + @Override + protected boolean onCreateActionBar(Menu menu) { + MenuInflater inflater = ((Activity) mActivity).getMenuInflater(); + inflater.inflate(R.menu.photo, menu); + menu.findItem(R.id.action_slideshow).setVisible( + mMediaSet != null && !(mMediaSet instanceof MtpDevice)); + mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu); + mMenu = menu; + mShowBars = true; + updateMenuOperations(); + return true; + } + + @Override + protected boolean onItemSelected(MenuItem item) { + MediaItem current = mModel.getCurrentMediaItem(); + + if (current == null) { + // item is not ready, ignore + return true; + } + + int currentIndex = mModel.getCurrentIndex(); + Path path = current.getPath(); + + DataManager manager = mActivity.getDataManager(); + int action = item.getItemId(); + switch (action) { + case R.id.action_slideshow: { + Bundle data = new Bundle(); + data.putString(SlideshowPage.KEY_SET_PATH, + mMediaSet.getPath().toString()); + data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex); + data.putBoolean(SlideshowPage.KEY_REPEAT, true); + mActivity.getStateManager().startStateForResult( + SlideshowPage.class, REQUEST_SLIDESHOW, data); + return true; + } + case R.id.action_crop: { + Activity activity = (Activity) mActivity; + Intent intent = new Intent(CropImage.CROP_ACTION); + intent.setClass(activity, CropImage.class); + intent.setData(manager.getContentUri(path)); + activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current) + ? REQUEST_CROP_PICASA + : REQUEST_CROP); + return true; + } + case R.id.action_details: { + if (mShowDetails) { + hideDetails(); + } else { + showDetails(currentIndex); + } + return true; + } + case R.id.action_setas: + case R.id.action_confirm_delete: + case R.id.action_rotate_ccw: + case R.id.action_rotate_cw: + case R.id.action_show_on_map: + case R.id.action_edit: + mSelectionManager.deSelectAll(); + mSelectionManager.toggle(path); + mMenuExecutor.onMenuClicked(item, null); + return true; + case R.id.action_import: + mSelectionManager.deSelectAll(); + mSelectionManager.toggle(path); + mMenuExecutor.onMenuClicked(item, + new ImportCompleteListener(mActivity)); + return true; + default : + return false; + } + } + + private void hideDetails() { + mShowDetails = false; + mDetailsWindow.hide(); + } + + private void showDetails(int index) { + mShowDetails = true; + if (mDetailsWindow == null) { + mDetailsWindow = new DetailsWindow(mActivity, new MyDetailsSource()); + mDetailsWindow.setCloseListener(new CloseListener() { + public void onClose() { + hideDetails(); + } + }); + mRootPane.addComponent(mDetailsWindow); + } + mDetailsWindow.reloadDetails(index); + mDetailsWindow.show(); + } + + public void onSingleTapUp(int x, int y) { + MediaItem item = mModel.getCurrentMediaItem(); + if (item == null) { + // item is not ready, ignore + return; + } + + boolean playVideo = + (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0; + + if (playVideo) { + // determine if the point is at center (1/6) of the photo view. + // (The position of the "play" icon is at center (1/6) of the photo) + int w = mPhotoView.getWidth(); + int h = mPhotoView.getHeight(); + playVideo = (Math.abs(x - w / 2) * 12 <= w) + && (Math.abs(y - h / 2) * 12 <= h); + } + + if (playVideo) { + playVideo((Activity) mActivity, item.getPlayUri(), item.getName()); + } else { + onUserInteractionTap(); + } + } + + public static void playVideo(Activity activity, Uri uri, String title) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW) + .setDataAndType(uri, "video/*"); + intent.putExtra(Intent.EXTRA_TITLE, title); + activity.startActivity(intent); + } catch (ActivityNotFoundException e) { + Toast.makeText(activity, activity.getString(R.string.video_err), + Toast.LENGTH_SHORT).show(); + } + } + + // Called by FileStripView + public void onSlotSelected(int slotIndex) { + ((PhotoDataAdapter) mModel).jumpTo(slotIndex); + } + + @Override + protected void onStateResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CROP: + if (resultCode == Activity.RESULT_OK) { + if (data == null) break; + Path path = mApplication + .getDataManager().findPathByUri(data.getData()); + if (path != null) { + mModel.setCurrentPhoto(path, mCurrentIndex); + } + } + break; + case REQUEST_CROP_PICASA: { + int message = resultCode == Activity.RESULT_OK + ? R.string.crop_saved + : R.string.crop_not_saved; + Toast.makeText(mActivity.getAndroidContext(), + message, Toast.LENGTH_SHORT).show(); + break; + } + case REQUEST_SLIDESHOW: { + if (data == null) break; + String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH); + int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0); + if (path != null) { + mModel.setCurrentPhoto(Path.fromString(path), index); + } + } + } + } + + @Override + public void onPause() { + super.onPause(); + mIsActive = false; + if (mFilmStripView != null) { + mFilmStripView.pause(); + } + if (mDetailsWindow != null) { + mDetailsWindow.pause(); + } + mPhotoView.pause(); + mModel.pause(); + mHandler.removeMessages(MSG_HIDE_BARS); + mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener); + } + + @Override + protected void onResume() { + super.onResume(); + mIsActive = true; + setContentPane(mRootPane); + mModel.resume(); + mPhotoView.resume(); + if (mFilmStripView != null) { + mFilmStripView.resume(); + } + if (mMenuVisibilityListener == null) { + mMenuVisibilityListener = new MyMenuVisibilityListener(); + } + mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener); + onUserInteraction(); + } + + private class MyDetailsSource implements DetailsSource { + public MediaDetails getDetails() { + return mModel.getCurrentMediaItem().getDetails(); + } + public int size() { + return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1; + } + public int findIndex(int indexHint) { + return indexHint; + } + } +} diff --git a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java new file mode 100644 index 000000000..11e0013cc --- /dev/null +++ b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.PhotoView; +import com.android.gallery3d.ui.PhotoView.ImageData; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.ui.TileImageViewAdapter; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Message; + +public class SinglePhotoDataAdapter extends TileImageViewAdapter + implements PhotoPage.Model { + + private static final String TAG = "SinglePhotoDataAdapter"; + private static final int SIZE_BACKUP = 640; + private static final int MSG_UPDATE_IMAGE = 1; + + private MediaItem mItem; + private boolean mHasFullImage; + private Future mTask; + private BitmapRegionDecoder mDecoder; + private Bitmap mBackup; + private Handler mHandler; + + private PhotoView mPhotoView; + private ThreadPool mThreadPool; + + public SinglePhotoDataAdapter( + GalleryActivity activity, PhotoView view, MediaItem item) { + mItem = Utils.checkNotNull(item); + mHasFullImage = (item.getSupportedOperations() & + MediaItem.SUPPORT_FULL_IMAGE) != 0; + mPhotoView = Utils.checkNotNull(view); + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + @SuppressWarnings("unchecked") + public void handleMessage(Message message) { + Utils.assertTrue(message.what == MSG_UPDATE_IMAGE); + if (mHasFullImage) { + onDecodeLargeComplete((Future) + message.obj); + } else { + onDecodeThumbComplete((Future) message.obj); + } + } + }; + mThreadPool = activity.getThreadPool(); + } + + private FutureListener mLargeListener = + new FutureListener() { + public void onFutureDone(Future future) { + mHandler.sendMessage( + mHandler.obtainMessage(MSG_UPDATE_IMAGE, future)); + } + }; + + private FutureListener mThumbListener = + new FutureListener() { + public void onFutureDone(Future future) { + mHandler.sendMessage( + mHandler.obtainMessage(MSG_UPDATE_IMAGE, future)); + } + }; + + public boolean isEmpty() { + return false; + } + + public int getImageRotation() { + return mItem.getRotation(); + } + + private void onDecodeLargeComplete(Future future) { + try { + mDecoder = future.get(); + if (mDecoder == null) return; + int width = mDecoder.getWidth(); + int height = mDecoder.getHeight(); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = BitmapUtils.computeSampleSizeLarger( + width, height, SIZE_BACKUP); + mBackup = mDecoder.decodeRegion( + new Rect(0, 0, width, height), options); + setBackupImage(mBackup, width, height); + setRegionDecoder(mDecoder); + mPhotoView.notifyImageInvalidated(0); + } catch (Throwable t) { + Log.w(TAG, "fail to decode large", t); + } + } + + private void onDecodeThumbComplete(Future future) { + try { + mBackup = future.get(); + if (mBackup == null) return; + setBackupImage(mBackup, mBackup.getWidth(), mBackup.getHeight()); + mPhotoView.notifyOnNewImage(); + mPhotoView.notifyImageInvalidated(0); // the current image + } catch (Throwable t) { + Log.w(TAG, "fail to decode thumb", t); + } + } + + public void resume() { + if (mTask == null) { + if (mHasFullImage) { + mTask = mThreadPool.submit( + mItem.requestLargeImage(), mLargeListener); + } else { + mTask = mThreadPool.submit( + mItem.requestImage(MediaItem.TYPE_THUMBNAIL), + mThumbListener); + } + } + } + + public void pause() { + Future task = mTask; + task.cancel(); + task.waitDone(); + if (task.get() == null) { + mTask = null; + } + } + + public ImageData getNextImage() { + return null; + } + + public ImageData getPreviousImage() { + return null; + } + + public void next() { + throw new UnsupportedOperationException(); + } + + public void previous() { + throw new UnsupportedOperationException(); + } + + public MediaItem getCurrentMediaItem() { + return mItem; + } + + public int getCurrentIndex() { + return 0; + } + + public void setCurrentPhoto(Path path, int indexHint) { + // ignore + } +} diff --git a/src/com/android/gallery3d/app/SlideshowDataAdapter.java b/src/com/android/gallery3d/app/SlideshowDataAdapter.java new file mode 100644 index 000000000..6f9b98e8e --- /dev/null +++ b/src/com/android/gallery3d/app/SlideshowDataAdapter.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.app.SlideshowPage.Slide; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.graphics.Bitmap; + +import java.util.LinkedList; +import java.util.concurrent.atomic.AtomicBoolean; + +public class SlideshowDataAdapter implements SlideshowPage.Model { + @SuppressWarnings("unused") + private static final String TAG = "SlideshowDataAdapter"; + + private static final int IMAGE_QUEUE_CAPACITY = 3; + + public interface SlideshowSource { + public void addContentListener(ContentListener listener); + public void removeContentListener(ContentListener listener); + public long reload(); + public MediaItem getMediaItem(int index); + } + + private final SlideshowSource mSource; + + private int mLoadIndex = 0; + private int mNextOutput = 0; + private boolean mIsActive = false; + private boolean mNeedReset; + private boolean mDataReady; + + private final LinkedList mImageQueue = new LinkedList(); + + private Future mReloadTask; + private final ThreadPool mThreadPool; + + private long mDataVersion = MediaObject.INVALID_DATA_VERSION; + private final AtomicBoolean mNeedReload = new AtomicBoolean(false); + private final SourceListener mSourceListener = new SourceListener(); + + public SlideshowDataAdapter(GalleryContext context, SlideshowSource source, int index) { + mSource = source; + mLoadIndex = index; + mNextOutput = index; + mThreadPool = context.getThreadPool(); + } + + public MediaItem loadItem() { + if (mNeedReload.compareAndSet(true, false)) { + long v = mSource.reload(); + if (v != mDataVersion) { + mDataVersion = v; + mNeedReset = true; + return null; + } + } + return mSource.getMediaItem(mLoadIndex); + } + + private class ReloadTask implements Job { + public Void run(JobContext jc) { + while (true) { + synchronized (SlideshowDataAdapter.this) { + while (mIsActive && (!mDataReady + || mImageQueue.size() >= IMAGE_QUEUE_CAPACITY)) { + try { + SlideshowDataAdapter.this.wait(); + } catch (InterruptedException ex) { + // ignored. + } + continue; + } + } + if (!mIsActive) return null; + mNeedReset = false; + + MediaItem item = loadItem(); + + if (mNeedReset) { + synchronized (SlideshowDataAdapter.this) { + mImageQueue.clear(); + mLoadIndex = mNextOutput; + } + continue; + } + + if (item == null) { + synchronized (SlideshowDataAdapter.this) { + if (!mNeedReload.get()) mDataReady = false; + SlideshowDataAdapter.this.notifyAll(); + } + continue; + } + + Bitmap bitmap = item + .requestImage(MediaItem.TYPE_THUMBNAIL) + .run(jc); + + if (bitmap != null) { + synchronized (SlideshowDataAdapter.this) { + mImageQueue.addLast( + new Slide(item, mLoadIndex, bitmap)); + if (mImageQueue.size() == 1) { + SlideshowDataAdapter.this.notifyAll(); + } + } + } + ++mLoadIndex; + } + } + } + + private class SourceListener implements ContentListener { + public void onContentDirty() { + synchronized (SlideshowDataAdapter.this) { + mNeedReload.set(true); + mDataReady = true; + SlideshowDataAdapter.this.notifyAll(); + } + } + } + + private synchronized Slide innerNextBitmap() { + while (mIsActive && mDataReady && mImageQueue.isEmpty()) { + try { + wait(); + } catch (InterruptedException t) { + throw new AssertionError(); + } + } + if (mImageQueue.isEmpty()) return null; + mNextOutput++; + this.notifyAll(); + return mImageQueue.removeFirst(); + } + + public Future nextSlide(FutureListener listener) { + return mThreadPool.submit(new Job() { + public Slide run(JobContext jc) { + jc.setMode(ThreadPool.MODE_NONE); + return innerNextBitmap(); + } + }, listener); + } + + public void pause() { + synchronized (this) { + mIsActive = false; + notifyAll(); + } + mSource.removeContentListener(mSourceListener); + mReloadTask.cancel(); + mReloadTask.waitDone(); + mReloadTask = null; + } + + public synchronized void resume() { + mIsActive = true; + mSource.addContentListener(mSourceListener); + mNeedReload.set(true); + mDataReady = true; + mReloadTask = mThreadPool.submit(new ReloadTask()); + } +} diff --git a/src/com/android/gallery3d/app/SlideshowDream.java b/src/com/android/gallery3d/app/SlideshowDream.java new file mode 100644 index 000000000..f4abe86ab --- /dev/null +++ b/src/com/android/gallery3d/app/SlideshowDream.java @@ -0,0 +1,28 @@ +package com.android.gallery3d.app; + +import android.app.Activity; +import android.content.Intent; +import android.support.v13.dreams.BasicDream; +import android.graphics.Canvas; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.view.View; +import android.widget.ImageView; +import android.widget.ViewFlipper; + +public class SlideshowDream extends BasicDream { + @Override + public void onCreate(Bundle bndl) { + super.onCreate(bndl); + Intent i = new Intent( + Intent.ACTION_VIEW, + android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI) +// Uri.fromFile(Environment.getExternalStoragePublicDirectory( +// Environment.DIRECTORY_PICTURES))) + .putExtra(Gallery.EXTRA_SLIDESHOW, true) + .setFlags(getIntent().getFlags()); + startActivity(i); + finish(); + } +} diff --git a/src/com/android/gallery3d/app/SlideshowPage.java b/src/com/android/gallery3d/app/SlideshowPage.java new file mode 100644 index 000000000..cdf9308ec --- /dev/null +++ b/src/com/android/gallery3d/app/SlideshowPage.java @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.ui.GLCanvas; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.SlideshowView; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.view.MotionEvent; + +import java.util.ArrayList; +import java.util.Random; + +public class SlideshowPage extends ActivityState { + private static final String TAG = "SlideshowPage"; + + public static final String KEY_SET_PATH = "media-set-path"; + public static final String KEY_ITEM_PATH = "media-item-path"; + public static final String KEY_PHOTO_INDEX = "photo-index"; + public static final String KEY_RANDOM_ORDER = "random-order"; + public static final String KEY_REPEAT = "repeat"; + + private static final long SLIDESHOW_DELAY = 3000; // 3 seconds + + private static final int MSG_LOAD_NEXT_BITMAP = 1; + private static final int MSG_SHOW_PENDING_BITMAP = 2; + + public static interface Model { + public void pause(); + public void resume(); + public Future nextSlide(FutureListener listener); + } + + public static class Slide { + public Bitmap bitmap; + public MediaItem item; + public int index; + + public Slide(MediaItem item, int index, Bitmap bitmap) { + this.bitmap = bitmap; + this.item = item; + this.index = index; + } + } + + private Handler mHandler; + private Model mModel; + private SlideshowView mSlideshowView; + + private Slide mPendingSlide = null; + private boolean mIsActive = false; + private WakeLock mWakeLock; + private Intent mResultIntent = new Intent(); + + private GLView mRootPane = new GLView() { + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + mSlideshowView.layout(0, 0, right - left, bottom - top); + } + + @Override + protected boolean onTouch(MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_UP) { + onBackPressed(); + } + return true; + } + + @Override + protected void renderBackground(GLCanvas canvas) { + canvas.clearBuffer(); + } + }; + + @Override + public void onCreate(Bundle data, Bundle restoreState) { + mFlags |= (FLAG_HIDE_ACTION_BAR | FLAG_HIDE_STATUS_BAR); + + PowerManager pm = (PowerManager) mActivity.getAndroidContext() + .getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK + | PowerManager.ON_AFTER_RELEASE, TAG); + + mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_SHOW_PENDING_BITMAP: + showPendingBitmap(); + break; + case MSG_LOAD_NEXT_BITMAP: + loadNextBitmap(); + break; + default: throw new AssertionError(); + } + } + }; + initializeViews(); + initializeData(data); + } + + private void loadNextBitmap() { + mModel.nextSlide(new FutureListener() { + public void onFutureDone(Future future) { + mPendingSlide = future.get(); + mHandler.sendEmptyMessage(MSG_SHOW_PENDING_BITMAP); + } + }); + } + + private void showPendingBitmap() { + // mPendingBitmap could be null, if + // 1.) there is no more items + // 2.) mModel is paused + Slide slide = mPendingSlide; + if (slide == null) { + if (mIsActive) { + mActivity.getStateManager().finishState(SlideshowPage.this); + } + return; + } + + mSlideshowView.next(slide.bitmap, slide.item.getRotation()); + + setStateResult(Activity.RESULT_OK, mResultIntent + .putExtra(KEY_ITEM_PATH, slide.item.getPath().toString()) + .putExtra(KEY_PHOTO_INDEX, slide.index)); + mHandler.sendEmptyMessageDelayed(MSG_LOAD_NEXT_BITMAP, + SLIDESHOW_DELAY); + } + + @Override + public void onPause() { + super.onPause(); + mWakeLock.release(); + mIsActive = false; + mModel.pause(); + mSlideshowView.release(); + + mHandler.removeMessages(MSG_LOAD_NEXT_BITMAP); + mHandler.removeMessages(MSG_SHOW_PENDING_BITMAP); + } + + @Override + public void onResume() { + super.onResume(); + mWakeLock.acquire(); + mIsActive = true; + mModel.resume(); + + if (mPendingSlide != null) { + showPendingBitmap(); + } else { + loadNextBitmap(); + } + } + + private void initializeData(Bundle data) { + String mediaPath = data.getString(KEY_SET_PATH); + boolean random = data.getBoolean(KEY_RANDOM_ORDER, false); + MediaSet mediaSet = mActivity.getDataManager().getMediaSet(mediaPath); + + if (random) { + boolean repeat = data.getBoolean(KEY_REPEAT); + mModel = new SlideshowDataAdapter( + mActivity, new ShuffleSource(mediaSet, repeat), 0); + setStateResult(Activity.RESULT_OK, + mResultIntent.putExtra(KEY_PHOTO_INDEX, 0)); + } else { + int index = data.getInt(KEY_PHOTO_INDEX); + boolean repeat = data.getBoolean(KEY_REPEAT); + mModel = new SlideshowDataAdapter(mActivity, + new SequentialSource(mediaSet, repeat), index); + setStateResult(Activity.RESULT_OK, + mResultIntent.putExtra(KEY_PHOTO_INDEX, index)); + } + } + + private void initializeViews() { + mSlideshowView = new SlideshowView(); + mRootPane.addComponent(mSlideshowView); + setContentPane(mRootPane); + } + + private static MediaItem findMediaItem(MediaSet mediaSet, int index) { + for (int i = 0, n = mediaSet.getSubMediaSetCount(); i < n; ++i) { + MediaSet subset = mediaSet.getSubMediaSet(i); + int count = subset.getTotalMediaItemCount(); + if (index < count) { + return findMediaItem(subset, index); + } + index -= count; + } + ArrayList list = mediaSet.getMediaItem(index, 1); + return list.isEmpty() ? null : list.get(0); + } + + private static class ShuffleSource implements SlideshowDataAdapter.SlideshowSource { + private static final int RETRY_COUNT = 5; + private final MediaSet mMediaSet; + private final Random mRandom = new Random(); + private int mOrder[] = new int[0]; + private boolean mRepeat; + private long mSourceVersion = MediaSet.INVALID_DATA_VERSION; + private int mLastIndex = -1; + + public ShuffleSource(MediaSet mediaSet, boolean repeat) { + mMediaSet = Utils.checkNotNull(mediaSet); + mRepeat = repeat; + } + + public MediaItem getMediaItem(int index) { + if (!mRepeat && index >= mOrder.length) return null; + mLastIndex = mOrder[index % mOrder.length]; + MediaItem item = findMediaItem(mMediaSet, mLastIndex); + for (int i = 0; i < RETRY_COUNT && item == null; ++i) { + Log.w(TAG, "fail to find image: " + mLastIndex); + mLastIndex = mRandom.nextInt(mOrder.length); + item = findMediaItem(mMediaSet, mLastIndex); + } + return item; + } + + public long reload() { + long version = mMediaSet.reload(); + if (version != mSourceVersion) { + mSourceVersion = version; + int count = mMediaSet.getTotalMediaItemCount(); + if (count != mOrder.length) generateOrderArray(count); + } + return version; + } + + private void generateOrderArray(int totalCount) { + if (mOrder.length != totalCount) { + mOrder = new int[totalCount]; + for (int i = 0; i < totalCount; ++i) { + mOrder[i] = i; + } + } + for (int i = totalCount - 1; i > 0; --i) { + Utils.swap(mOrder, i, mRandom.nextInt(i + 1)); + } + if (mOrder[0] == mLastIndex && totalCount > 1) { + Utils.swap(mOrder, 0, mRandom.nextInt(totalCount - 1) + 1); + } + } + + public void addContentListener(ContentListener listener) { + mMediaSet.addContentListener(listener); + } + + public void removeContentListener(ContentListener listener) { + mMediaSet.removeContentListener(listener); + } + } + + private static class SequentialSource implements SlideshowDataAdapter.SlideshowSource { + private static final int DATA_SIZE = 32; + + private ArrayList mData = new ArrayList(); + private int mDataStart = 0; + private long mDataVersion = MediaObject.INVALID_DATA_VERSION; + private final MediaSet mMediaSet; + private final boolean mRepeat; + + public SequentialSource(MediaSet mediaSet, boolean repeat) { + mMediaSet = mediaSet; + mRepeat = repeat; + } + + public MediaItem getMediaItem(int index) { + int dataEnd = mDataStart + mData.size(); + + if (mRepeat) { + index = index % mMediaSet.getMediaItemCount(); + } + if (index < mDataStart || index >= dataEnd) { + mData = mMediaSet.getMediaItem(index, DATA_SIZE); + mDataStart = index; + dataEnd = index + mData.size(); + } + + return (index < mDataStart || index >= dataEnd) + ? null + : mData.get(index - mDataStart); + } + + public long reload() { + long version = mMediaSet.reload(); + if (version != mDataVersion) { + mDataVersion = version; + mData.clear(); + } + return mDataVersion; + } + + public void addContentListener(ContentListener listener) { + mMediaSet.addContentListener(listener); + } + + public void removeContentListener(ContentListener listener) { + mMediaSet.removeContentListener(listener); + } + } +} diff --git a/src/com/android/gallery3d/app/StateManager.java b/src/com/android/gallery3d/app/StateManager.java new file mode 100644 index 000000000..b551f693a --- /dev/null +++ b/src/com/android/gallery3d/app/StateManager.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import com.android.gallery3d.common.Utils; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Parcelable; +import android.view.Menu; +import android.view.MenuItem; + +import java.util.Stack; + +public class StateManager { + @SuppressWarnings("unused") + private static final String TAG = "StateManager"; + private boolean mIsResumed = false; + + private static final String KEY_MAIN = "activity-state"; + private static final String KEY_DATA = "data"; + private static final String KEY_STATE = "bundle"; + private static final String KEY_CLASS = "class"; + + private GalleryActivity mContext; + private Stack mStack = new Stack(); + private ActivityState.ResultEntry mResult; + + public StateManager(GalleryActivity context) { + mContext = context; + } + + public void startState(Class klass, + Bundle data) { + Log.v(TAG, "startState " + klass); + ActivityState state = null; + try { + state = klass.newInstance(); + } catch (Exception e) { + throw new AssertionError(e); + } + if (!mStack.isEmpty()) { + ActivityState top = getTopState(); + if (mIsResumed) top.onPause(); + } + state.initialize(mContext, data); + + mStack.push(new StateEntry(data, state)); + state.onCreate(data, null); + if (mIsResumed) state.resume(); + } + + public void startStateForResult(Class klass, + int requestCode, Bundle data) { + Log.v(TAG, "startStateForResult " + klass + ", " + requestCode); + ActivityState state = null; + try { + state = klass.newInstance(); + } catch (Exception e) { + throw new AssertionError(e); + } + state.initialize(mContext, data); + state.mResult = new ActivityState.ResultEntry(); + state.mResult.requestCode = requestCode; + + if (!mStack.isEmpty()) { + ActivityState as = getTopState(); + as.mReceivedResults = state.mResult; + if (mIsResumed) as.onPause(); + } else { + mResult = state.mResult; + } + + mStack.push(new StateEntry(data, state)); + state.onCreate(data, null); + if (mIsResumed) state.resume(); + } + + public boolean createOptionsMenu(Menu menu) { + if (!mStack.isEmpty()) { + ((Activity) mContext).setProgressBarIndeterminateVisibility(false); + return getTopState().onCreateActionBar(menu); + } else { + return false; + } + } + + public void resume() { + if (mIsResumed) return; + mIsResumed = true; + if (!mStack.isEmpty()) getTopState().resume(); + } + + public void pause() { + if (!mIsResumed) return; + mIsResumed = false; + if (!mStack.isEmpty()) getTopState().onPause(); + } + + public void notifyActivityResult(int requestCode, int resultCode, Intent data) { + getTopState().onStateResult(requestCode, resultCode, data); + } + + public int getStateCount() { + return mStack.size(); + } + + public boolean itemSelected(MenuItem item) { + if (!mStack.isEmpty()) { + if (mStack.size() > 1 && item.getItemId() == android.R.id.home) { + getTopState().onBackPressed(); + return true; + } else { + return getTopState().onItemSelected(item); + } + } + return false; + } + + public void onBackPressed() { + if (!mStack.isEmpty()) { + getTopState().onBackPressed(); + } + } + + void finishState(ActivityState state) { + Log.v(TAG, "finishState " + state.getClass()); + if (state != mStack.peek().activityState) { + throw new IllegalArgumentException("The stateview to be finished" + + " is not at the top of the stack: " + state + ", " + + mStack.peek().activityState); + } + + // Remove the top state. + mStack.pop(); + if (mIsResumed) state.onPause(); + mContext.getGLRoot().setContentPane(null); + state.onDestroy(); + + if (mStack.isEmpty()) { + Log.v(TAG, "no more state, finish activity"); + Activity activity = (Activity) mContext.getAndroidContext(); + if (mResult != null) { + activity.setResult(mResult.resultCode, mResult.resultData); + } + activity.finish(); + + // The finish() request is rejected (only happens under Monkey), + // so we start the default page instead. + if (!activity.isFinishing()) { + Log.v(TAG, "finish() failed, start default page"); + ((Gallery) mContext).startDefaultPage(); + } + } else { + // Restore the immediately previous state + ActivityState top = mStack.peek().activityState; + if (mIsResumed) top.resume(); + } + } + + void switchState(ActivityState oldState, + Class klass, Bundle data) { + Log.v(TAG, "switchState " + oldState + ", " + klass); + if (oldState != mStack.peek().activityState) { + throw new IllegalArgumentException("The stateview to be finished" + + " is not at the top of the stack: " + oldState + ", " + + mStack.peek().activityState); + } + // Remove the top state. + mStack.pop(); + if (mIsResumed) oldState.onPause(); + oldState.onDestroy(); + + // Create new state. + ActivityState state = null; + try { + state = klass.newInstance(); + } catch (Exception e) { + throw new AssertionError(e); + } + state.initialize(mContext, data); + mStack.push(new StateEntry(data, state)); + state.onCreate(data, null); + if (mIsResumed) state.resume(); + } + + public void destroy() { + Log.v(TAG, "destroy"); + while (!mStack.isEmpty()) { + mStack.pop().activityState.onDestroy(); + } + mStack.clear(); + } + + @SuppressWarnings("unchecked") + public void restoreFromState(Bundle inState) { + Log.v(TAG, "restoreFromState"); + Parcelable list[] = inState.getParcelableArray(KEY_MAIN); + + for (Parcelable parcelable : list) { + Bundle bundle = (Bundle) parcelable; + Class klass = + (Class) bundle.getSerializable(KEY_CLASS); + + Bundle data = bundle.getBundle(KEY_DATA); + Bundle state = bundle.getBundle(KEY_STATE); + + ActivityState activityState; + try { + Log.v(TAG, "restoreFromState " + klass); + activityState = klass.newInstance(); + } catch (Exception e) { + throw new AssertionError(e); + } + activityState.initialize(mContext, data); + activityState.onCreate(data, state); + mStack.push(new StateEntry(data, activityState)); + } + } + + public void saveState(Bundle outState) { + Log.v(TAG, "saveState"); + Parcelable list[] = new Parcelable[mStack.size()]; + + int i = 0; + for (StateEntry entry : mStack) { + Bundle bundle = new Bundle(); + bundle.putSerializable(KEY_CLASS, entry.activityState.getClass()); + bundle.putBundle(KEY_DATA, entry.data); + Bundle state = new Bundle(); + entry.activityState.onSaveState(state); + bundle.putBundle(KEY_STATE, state); + Log.v(TAG, "saveState " + entry.activityState.getClass()); + list[i++] = bundle; + } + outState.putParcelableArray(KEY_MAIN, list); + } + + public boolean hasStateClass(Class klass) { + for (StateEntry entry : mStack) { + if (klass.isInstance(entry.activityState)) { + return true; + } + } + return false; + } + + public ActivityState getTopState() { + Utils.assertTrue(!mStack.isEmpty()); + return mStack.peek().activityState; + } + + private static class StateEntry { + public Bundle data; + public ActivityState activityState; + + public StateEntry(Bundle data, ActivityState state) { + this.data = data; + this.activityState = state; + } + } +} diff --git a/src/com/android/gallery3d/app/UsbDeviceActivity.java b/src/com/android/gallery3d/app/UsbDeviceActivity.java new file mode 100644 index 000000000..28bd667e3 --- /dev/null +++ b/src/com/android/gallery3d/app/UsbDeviceActivity.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +/* This Activity does nothing but receive USB_DEVICE_ATTACHED events from the + * USB service and springboards to the main Gallery activity + */ +public final class UsbDeviceActivity extends Activity { + + static final String TAG = "UsbDeviceActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // + Intent intent = new Intent(this, Gallery.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + try { + startActivity(intent); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "unable to start Gallery activity", e); + } + finish(); + } +} diff --git a/src/com/android/gallery3d/app/Wallpaper.java b/src/com/android/gallery3d/app/Wallpaper.java new file mode 100644 index 000000000..07a3d5313 --- /dev/null +++ b/src/com/android/gallery3d/app/Wallpaper.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.app; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.Display; + +/** + * Wallpaper picker for the gallery application. This just redirects to the + * standard pick action. + */ +public class Wallpaper extends Activity { + @SuppressWarnings("unused") + private static final String TAG = "Wallpaper"; + + private static final String IMAGE_TYPE = "image/*"; + private static final String KEY_STATE = "activity-state"; + private static final String KEY_PICKED_ITEM = "picked-item"; + + private static final int STATE_INIT = 0; + private static final int STATE_PHOTO_PICKED = 1; + + private int mState = STATE_INIT; + private Uri mPickedItem; + + @Override + protected void onCreate(Bundle bundle) { + super.onCreate(bundle); + if (bundle != null) { + mState = bundle.getInt(KEY_STATE); + mPickedItem = (Uri) bundle.getParcelable(KEY_PICKED_ITEM); + } + } + + @Override + protected void onSaveInstanceState(Bundle saveState) { + saveState.putInt(KEY_STATE, mState); + if (mPickedItem != null) { + saveState.putParcelable(KEY_PICKED_ITEM, mPickedItem); + } + } + + @SuppressWarnings("fallthrough") + @Override + protected void onResume() { + super.onResume(); + Intent intent = getIntent(); + switch (mState) { + case STATE_INIT: { + mPickedItem = intent.getData(); + if (mPickedItem == null) { + Intent request = new Intent(Intent.ACTION_GET_CONTENT) + .setClass(this, DialogPicker.class) + .setType(IMAGE_TYPE); + startActivityForResult(request, STATE_PHOTO_PICKED); + return; + } + mState = STATE_PHOTO_PICKED; + // fall-through + } + case STATE_PHOTO_PICKED: { + int width = getWallpaperDesiredMinimumWidth(); + int height = getWallpaperDesiredMinimumHeight(); + Display display = getWindowManager().getDefaultDisplay(); + float spotlightX = (float) display.getWidth() / width; + float spotlightY = (float) display.getHeight() / height; + Intent request = new Intent(CropImage.ACTION_CROP) + .setDataAndType(mPickedItem, IMAGE_TYPE) + .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) + .putExtra(CropImage.KEY_OUTPUT_X, width) + .putExtra(CropImage.KEY_OUTPUT_Y, height) + .putExtra(CropImage.KEY_ASPECT_X, width) + .putExtra(CropImage.KEY_ASPECT_Y, height) + .putExtra(CropImage.KEY_SPOTLIGHT_X, spotlightX) + .putExtra(CropImage.KEY_SPOTLIGHT_Y, spotlightY) + .putExtra(CropImage.KEY_SCALE, true) + .putExtra(CropImage.KEY_NO_FACE_DETECTION, true) + .putExtra(CropImage.KEY_SET_AS_WALLPAPER, true); + startActivity(request); + finish(); + } + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode != RESULT_OK) { + setResult(resultCode); + finish(); + return; + } + mState = requestCode; + if (mState == STATE_PHOTO_PICKED) { + mPickedItem = data.getData(); + } + + // onResume() would be called next + } +} diff --git a/src/com/android/gallery3d/data/ChangeNotifier.java b/src/com/android/gallery3d/data/ChangeNotifier.java new file mode 100644 index 000000000..e1e601dd6 --- /dev/null +++ b/src/com/android/gallery3d/data/ChangeNotifier.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.GalleryApp; + +import android.net.Uri; + +import java.util.concurrent.atomic.AtomicBoolean; + +// This handles change notification for media sets. +public class ChangeNotifier { + + private MediaSet mMediaSet; + private AtomicBoolean mContentDirty = new AtomicBoolean(true); + + public ChangeNotifier(MediaSet set, Uri uri, GalleryApp application) { + mMediaSet = set; + application.getDataManager().registerChangeNotifier(uri, this); + } + + // Returns the dirty flag and clear it. + public boolean isDirty() { + return mContentDirty.compareAndSet(true, false); + } + + public void fakeChange() { + onChange(false); + } + + public void clearDirty() { + mContentDirty.set(false); + } + + protected void onChange(boolean selfChange) { + if (mContentDirty.compareAndSet(false, true)) { + mMediaSet.notifyContentChanged(); + } + } +} \ No newline at end of file diff --git a/src/com/android/gallery3d/data/ClusterAlbum.java b/src/com/android/gallery3d/data/ClusterAlbum.java new file mode 100644 index 000000000..32f902301 --- /dev/null +++ b/src/com/android/gallery3d/data/ClusterAlbum.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import java.util.ArrayList; + +public class ClusterAlbum extends MediaSet implements ContentListener { + private static final String TAG = "ClusterAlbum"; + private ArrayList mPaths = new ArrayList(); + private String mName = ""; + private DataManager mDataManager; + private MediaSet mClusterAlbumSet; + + public ClusterAlbum(Path path, DataManager dataManager, + MediaSet clusterAlbumSet) { + super(path, nextVersionNumber()); + mDataManager = dataManager; + mClusterAlbumSet = clusterAlbumSet; + mClusterAlbumSet.addContentListener(this); + } + + void setMediaItems(ArrayList paths) { + mPaths = paths; + } + + ArrayList getMediaItems() { + return mPaths; + } + + public void setName(String name) { + mName = name; + } + + @Override + public String getName() { + return mName; + } + + @Override + public int getMediaItemCount() { + return mPaths.size(); + } + + @Override + public ArrayList getMediaItem(int start, int count) { + return getMediaItemFromPath(mPaths, start, count, mDataManager); + } + + public static ArrayList getMediaItemFromPath( + ArrayList paths, int start, int count, + DataManager dataManager) { + if (start >= paths.size()) { + return new ArrayList(); + } + int end = Math.min(start + count, paths.size()); + ArrayList subset = new ArrayList(paths.subList(start, end)); + final MediaItem[] buf = new MediaItem[end - start]; + ItemConsumer consumer = new ItemConsumer() { + public void consume(int index, MediaItem item) { + buf[index] = item; + } + }; + dataManager.mapMediaItems(subset, consumer, 0); + ArrayList result = new ArrayList(end - start); + for (int i = 0; i < buf.length; i++) { + result.add(buf[i]); + } + return result; + } + + @Override + protected int enumerateMediaItems(ItemConsumer consumer, int startIndex) { + mDataManager.mapMediaItems(mPaths, consumer, startIndex); + return mPaths.size(); + } + + @Override + public int getTotalMediaItemCount() { + return mPaths.size(); + } + + @Override + public long reload() { + if (mClusterAlbumSet.reload() > mDataVersion) { + mDataVersion = nextVersionNumber(); + } + return mDataVersion; + } + + public void onContentDirty() { + notifyContentChanged(); + } + + @Override + public int getSupportedOperations() { + return SUPPORT_SHARE | SUPPORT_DELETE | SUPPORT_INFO; + } + + @Override + public void delete() { + ItemConsumer consumer = new ItemConsumer() { + public void consume(int index, MediaItem item) { + if ((item.getSupportedOperations() & SUPPORT_DELETE) != 0) { + item.delete(); + } + } + }; + mDataManager.mapMediaItems(mPaths, consumer, 0); + } + + @Override + public boolean isLeafAlbum() { + return true; + } +} diff --git a/src/com/android/gallery3d/data/ClusterAlbumSet.java b/src/com/android/gallery3d/data/ClusterAlbumSet.java new file mode 100644 index 000000000..5b0569a67 --- /dev/null +++ b/src/com/android/gallery3d/data/ClusterAlbumSet.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.GalleryApp; + +import android.content.Context; +import android.net.Uri; + +import java.util.ArrayList; +import java.util.HashSet; + +public class ClusterAlbumSet extends MediaSet implements ContentListener { + private static final String TAG = "ClusterAlbumSet"; + private GalleryApp mApplication; + private MediaSet mBaseSet; + private int mKind; + private ArrayList mAlbums = new ArrayList(); + private boolean mFirstReloadDone; + + public ClusterAlbumSet(Path path, GalleryApp application, + MediaSet baseSet, int kind) { + super(path, INVALID_DATA_VERSION); + mApplication = application; + mBaseSet = baseSet; + mKind = kind; + baseSet.addContentListener(this); + } + + @Override + public MediaSet getSubMediaSet(int index) { + return mAlbums.get(index); + } + + @Override + public int getSubMediaSetCount() { + return mAlbums.size(); + } + + @Override + public String getName() { + return mBaseSet.getName(); + } + + @Override + public long reload() { + if (mBaseSet.reload() > mDataVersion) { + if (mFirstReloadDone) { + updateClustersContents(); + } else { + updateClusters(); + mFirstReloadDone = true; + } + mDataVersion = nextVersionNumber(); + } + return mDataVersion; + } + + public void onContentDirty() { + notifyContentChanged(); + } + + private void updateClusters() { + mAlbums.clear(); + Clustering clustering; + Context context = mApplication.getAndroidContext(); + switch (mKind) { + case ClusterSource.CLUSTER_ALBUMSET_TIME: + clustering = new TimeClustering(context); + break; + case ClusterSource.CLUSTER_ALBUMSET_LOCATION: + clustering = new LocationClustering(context); + break; + case ClusterSource.CLUSTER_ALBUMSET_TAG: + clustering = new TagClustering(context); + break; + case ClusterSource.CLUSTER_ALBUMSET_FACE: + clustering = new FaceClustering(context); + break; + default: /* CLUSTER_ALBUMSET_SIZE */ + clustering = new SizeClustering(context); + break; + } + + clustering.run(mBaseSet); + int n = clustering.getNumberOfClusters(); + DataManager dataManager = mApplication.getDataManager(); + for (int i = 0; i < n; i++) { + Path childPath; + String childName = clustering.getClusterName(i); + if (mKind == ClusterSource.CLUSTER_ALBUMSET_TAG) { + childPath = mPath.getChild(Uri.encode(childName)); + } else if (mKind == ClusterSource.CLUSTER_ALBUMSET_SIZE) { + long minSize = ((SizeClustering) clustering).getMinSize(i); + childPath = mPath.getChild(minSize); + } else { + childPath = mPath.getChild(i); + } + ClusterAlbum album = (ClusterAlbum) dataManager.peekMediaObject( + childPath); + if (album == null) { + album = new ClusterAlbum(childPath, dataManager, this); + } + album.setMediaItems(clustering.getCluster(i)); + album.setName(childName); + mAlbums.add(album); + } + } + + private void updateClustersContents() { + final HashSet existing = new HashSet(); + mBaseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + public void consume(int index, MediaItem item) { + existing.add(item.getPath()); + } + }); + + int n = mAlbums.size(); + + // The loop goes backwards because we may remove empty albums from + // mAlbums. + for (int i = n - 1; i >= 0; i--) { + ArrayList oldPaths = mAlbums.get(i).getMediaItems(); + ArrayList newPaths = new ArrayList(); + int m = oldPaths.size(); + for (int j = 0; j < m; j++) { + Path p = oldPaths.get(j); + if (existing.contains(p)) { + newPaths.add(p); + } + } + mAlbums.get(i).setMediaItems(newPaths); + if (newPaths.isEmpty()) { + mAlbums.remove(i); + } + } + } +} diff --git a/src/com/android/gallery3d/data/ClusterSource.java b/src/com/android/gallery3d/data/ClusterSource.java new file mode 100644 index 000000000..a1f22e57a --- /dev/null +++ b/src/com/android/gallery3d/data/ClusterSource.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.GalleryApp; + +class ClusterSource extends MediaSource { + static final int CLUSTER_ALBUMSET_TIME = 0; + static final int CLUSTER_ALBUMSET_LOCATION = 1; + static final int CLUSTER_ALBUMSET_TAG = 2; + static final int CLUSTER_ALBUMSET_SIZE = 3; + static final int CLUSTER_ALBUMSET_FACE = 4; + + static final int CLUSTER_ALBUM_TIME = 0x100; + static final int CLUSTER_ALBUM_LOCATION = 0x101; + static final int CLUSTER_ALBUM_TAG = 0x102; + static final int CLUSTER_ALBUM_SIZE = 0x103; + static final int CLUSTER_ALBUM_FACE = 0x104; + + GalleryApp mApplication; + PathMatcher mMatcher; + + public ClusterSource(GalleryApp application) { + super("cluster"); + mApplication = application; + mMatcher = new PathMatcher(); + mMatcher.add("/cluster/*/time", CLUSTER_ALBUMSET_TIME); + mMatcher.add("/cluster/*/location", CLUSTER_ALBUMSET_LOCATION); + mMatcher.add("/cluster/*/tag", CLUSTER_ALBUMSET_TAG); + mMatcher.add("/cluster/*/size", CLUSTER_ALBUMSET_SIZE); + mMatcher.add("/cluster/*/face", CLUSTER_ALBUMSET_FACE); + + mMatcher.add("/cluster/*/time/*", CLUSTER_ALBUM_TIME); + mMatcher.add("/cluster/*/location/*", CLUSTER_ALBUM_LOCATION); + mMatcher.add("/cluster/*/tag/*", CLUSTER_ALBUM_TAG); + mMatcher.add("/cluster/*/size/*", CLUSTER_ALBUM_SIZE); + mMatcher.add("/cluster/*/face/*", CLUSTER_ALBUM_FACE); + } + + // The names we accept are: + // /cluster/{set}/time /cluster/{set}/time/k + // /cluster/{set}/location /cluster/{set}/location/k + // /cluster/{set}/tag /cluster/{set}/tag/encoded_tag + // /cluster/{set}/size /cluster/{set}/size/min_size + @Override + public MediaObject createMediaObject(Path path) { + int matchType = mMatcher.match(path); + String setsName = mMatcher.getVar(0); + DataManager dataManager = mApplication.getDataManager(); + MediaSet[] sets = dataManager.getMediaSetsFromString(setsName); + switch (matchType) { + case CLUSTER_ALBUMSET_TIME: + case CLUSTER_ALBUMSET_LOCATION: + case CLUSTER_ALBUMSET_TAG: + case CLUSTER_ALBUMSET_SIZE: + case CLUSTER_ALBUMSET_FACE: + return new ClusterAlbumSet(path, mApplication, sets[0], matchType); + case CLUSTER_ALBUM_TIME: + case CLUSTER_ALBUM_LOCATION: + case CLUSTER_ALBUM_TAG: + case CLUSTER_ALBUM_SIZE: + case CLUSTER_ALBUM_FACE: { + MediaSet parent = dataManager.getMediaSet(path.getParent()); + // The actual content in the ClusterAlbum will be filled later + // when the reload() method in the parent is run. + return new ClusterAlbum(path, dataManager, parent); + } + default: + throw new RuntimeException("bad path: " + path); + } + } +} diff --git a/src/com/android/gallery3d/data/Clustering.java b/src/com/android/gallery3d/data/Clustering.java new file mode 100644 index 000000000..542dda27f --- /dev/null +++ b/src/com/android/gallery3d/data/Clustering.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import java.util.ArrayList; + +public abstract class Clustering { + public abstract void run(MediaSet baseSet); + public abstract int getNumberOfClusters(); + public abstract ArrayList getCluster(int index); + public abstract String getClusterName(int index); +} diff --git a/src/com/android/gallery3d/data/ComboAlbum.java b/src/com/android/gallery3d/data/ComboAlbum.java new file mode 100644 index 000000000..8ca2077a4 --- /dev/null +++ b/src/com/android/gallery3d/data/ComboAlbum.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.GalleryApp; + +import java.util.ArrayList; + +// ComboAlbum combines multiple media sets into one. It lists all media items +// from the input albums. +// This only handles SubMediaSets, not MediaItems. (That's all we need now) +public class ComboAlbum extends MediaSet implements ContentListener { + private static final String TAG = "ComboAlbum"; + private final MediaSet[] mSets; + private final String mName; + + public ComboAlbum(Path path, MediaSet[] mediaSets, String name) { + super(path, nextVersionNumber()); + mSets = mediaSets; + for (MediaSet set : mSets) { + set.addContentListener(this); + } + mName = name; + } + + @Override + public ArrayList getMediaItem(int start, int count) { + ArrayList items = new ArrayList(); + for (MediaSet set : mSets) { + int size = set.getMediaItemCount(); + if (count < 1) break; + if (start < size) { + int fetchCount = (start + count <= size) ? count : size - start; + ArrayList fetchItems = set.getMediaItem(start, fetchCount); + items.addAll(fetchItems); + count -= fetchItems.size(); + start = 0; + } else { + start -= size; + } + } + return items; + } + + @Override + public int getMediaItemCount() { + int count = 0; + for (MediaSet set : mSets) { + count += set.getMediaItemCount(); + } + return count; + } + + @Override + public String getName() { + return mName; + } + + @Override + public long reload() { + boolean changed = false; + for (int i = 0, n = mSets.length; i < n; ++i) { + long version = mSets[i].reload(); + if (version > mDataVersion) changed = true; + } + if (changed) mDataVersion = nextVersionNumber(); + return mDataVersion; + } + + public void onContentDirty() { + notifyContentChanged(); + } +} diff --git a/src/com/android/gallery3d/data/ComboAlbumSet.java b/src/com/android/gallery3d/data/ComboAlbumSet.java new file mode 100644 index 000000000..aa196039d --- /dev/null +++ b/src/com/android/gallery3d/data/ComboAlbumSet.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; + +// ComboAlbumSet combines multiple media sets into one. It lists all sub +// media sets from the input album sets. +// This only handles SubMediaSets, not MediaItems. (That's all we need now) +public class ComboAlbumSet extends MediaSet implements ContentListener { + private static final String TAG = "ComboAlbumSet"; + private final MediaSet[] mSets; + private final String mName; + + public ComboAlbumSet(Path path, GalleryApp application, MediaSet[] mediaSets) { + super(path, nextVersionNumber()); + mSets = mediaSets; + for (MediaSet set : mSets) { + set.addContentListener(this); + } + mName = application.getResources().getString( + R.string.set_label_all_albums); + } + + @Override + public MediaSet getSubMediaSet(int index) { + for (MediaSet set : mSets) { + int size = set.getSubMediaSetCount(); + if (index < size) { + return set.getSubMediaSet(index); + } + index -= size; + } + return null; + } + + @Override + public int getSubMediaSetCount() { + int count = 0; + for (MediaSet set : mSets) { + count += set.getSubMediaSetCount(); + } + return count; + } + + @Override + public String getName() { + return mName; + } + + @Override + public long reload() { + boolean changed = false; + for (int i = 0, n = mSets.length; i < n; ++i) { + long version = mSets[i].reload(); + if (version > mDataVersion) changed = true; + } + if (changed) mDataVersion = nextVersionNumber(); + return mDataVersion; + } + + public void onContentDirty() { + notifyContentChanged(); + } +} diff --git a/src/com/android/gallery3d/data/ComboSource.java b/src/com/android/gallery3d/data/ComboSource.java new file mode 100644 index 000000000..867d47e64 --- /dev/null +++ b/src/com/android/gallery3d/data/ComboSource.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.GalleryApp; + +class ComboSource extends MediaSource { + private static final int COMBO_ALBUMSET = 0; + private static final int COMBO_ALBUM = 1; + private GalleryApp mApplication; + private PathMatcher mMatcher; + + public ComboSource(GalleryApp application) { + super("combo"); + mApplication = application; + mMatcher = new PathMatcher(); + mMatcher.add("/combo/*", COMBO_ALBUMSET); + mMatcher.add("/combo/*/*", COMBO_ALBUM); + } + + // The only path we accept is "/combo/{set1, set2, ...} and /combo/item/{set1, set2, ...}" + @Override + public MediaObject createMediaObject(Path path) { + String[] segments = path.split(); + if (segments.length < 2) { + throw new RuntimeException("bad path: " + path); + } + + DataManager dataManager = mApplication.getDataManager(); + switch (mMatcher.match(path)) { + case COMBO_ALBUMSET: + return new ComboAlbumSet(path, mApplication, + dataManager.getMediaSetsFromString(segments[1])); + + case COMBO_ALBUM: + return new ComboAlbum(path, + dataManager.getMediaSetsFromString(segments[2]), segments[1]); + } + return null; + } +} diff --git a/src/com/android/gallery3d/data/ContentListener.java b/src/com/android/gallery3d/data/ContentListener.java new file mode 100644 index 000000000..5e2952685 --- /dev/null +++ b/src/com/android/gallery3d/data/ContentListener.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +public interface ContentListener { + public void onContentDirty(); +} \ No newline at end of file diff --git a/src/com/android/gallery3d/data/DataManager.java b/src/com/android/gallery3d/data/DataManager.java new file mode 100644 index 000000000..f7dac5ebd --- /dev/null +++ b/src/com/android/gallery3d/data/DataManager.java @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaSet.ItemConsumer; +import com.android.gallery3d.data.MediaSource.PathId; +import com.android.gallery3d.picasasource.PicasaSource; + +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map.Entry; +import java.util.WeakHashMap; + +// DataManager manages all media sets and media items in the system. +// +// Each MediaSet and MediaItem has a unique 64 bits id. The most significant +// 32 bits represents its parent, and the least significant 32 bits represents +// the self id. For MediaSet the self id is is globally unique, but for +// MediaItem it's unique only relative to its parent. +// +// To make sure the id is the same when the MediaSet is re-created, a child key +// is provided to obtainSetId() to make sure the same self id will be used as +// when the parent and key are the same. A sequence of child keys is called a +// path. And it's used to identify a specific media set even if the process is +// killed and re-created, so child keys should be stable identifiers. + +public class DataManager { + public static final int INCLUDE_IMAGE = 1; + public static final int INCLUDE_VIDEO = 2; + public static final int INCLUDE_ALL = INCLUDE_IMAGE | INCLUDE_VIDEO; + public static final int INCLUDE_LOCAL_ONLY = 4; + public static final int INCLUDE_LOCAL_IMAGE_ONLY = + INCLUDE_LOCAL_ONLY | INCLUDE_IMAGE; + public static final int INCLUDE_LOCAL_VIDEO_ONLY = + INCLUDE_LOCAL_ONLY | INCLUDE_VIDEO; + public static final int INCLUDE_LOCAL_ALL_ONLY = + INCLUDE_LOCAL_ONLY | INCLUDE_IMAGE | INCLUDE_VIDEO; + + // Any one who would like to access data should require this lock + // to prevent concurrency issue. + public static final Object LOCK = new Object(); + + private static final String TAG = "DataManager"; + + // This is the path for the media set seen by the user at top level. + private static final String TOP_SET_PATH = + "/combo/{/mtp,/local/all,/picasa/all}"; + private static final String TOP_IMAGE_SET_PATH = + "/combo/{/mtp,/local/image,/picasa/image}"; + private static final String TOP_VIDEO_SET_PATH = + "/combo/{/local/video,/picasa/video}"; + private static final String TOP_LOCAL_SET_PATH = + "/local/all"; + private static final String TOP_LOCAL_IMAGE_SET_PATH = + "/local/image"; + private static final String TOP_LOCAL_VIDEO_SET_PATH = + "/local/video"; + + public static final Comparator sDateTakenComparator = + new DateTakenComparator(); + + private static class DateTakenComparator implements Comparator { + public int compare(MediaItem item1, MediaItem item2) { + return -Utils.compare(item1.getDateInMs(), item2.getDateInMs()); + } + } + + private final Handler mDefaultMainHandler; + + private GalleryApp mApplication; + private int mActiveCount = 0; + + private HashMap mNotifierMap = + new HashMap(); + + + private HashMap mSourceMap = + new LinkedHashMap(); + + public DataManager(GalleryApp application) { + mApplication = application; + mDefaultMainHandler = new Handler(application.getMainLooper()); + } + + public synchronized void initializeSourceMap() { + if (!mSourceMap.isEmpty()) return; + + // the order matters, the UriSource must come last + addSource(new LocalSource(mApplication)); + addSource(new PicasaSource(mApplication)); + addSource(new MtpSource(mApplication)); + addSource(new ComboSource(mApplication)); + addSource(new ClusterSource(mApplication)); + addSource(new FilterSource(mApplication)); + addSource(new UriSource(mApplication)); + + if (mActiveCount > 0) { + for (MediaSource source : mSourceMap.values()) { + source.resume(); + } + } + } + + public String getTopSetPath(int typeBits) { + + switch (typeBits) { + case INCLUDE_IMAGE: return TOP_IMAGE_SET_PATH; + case INCLUDE_VIDEO: return TOP_VIDEO_SET_PATH; + case INCLUDE_ALL: return TOP_SET_PATH; + case INCLUDE_LOCAL_IMAGE_ONLY: return TOP_LOCAL_IMAGE_SET_PATH; + case INCLUDE_LOCAL_VIDEO_ONLY: return TOP_LOCAL_VIDEO_SET_PATH; + case INCLUDE_LOCAL_ALL_ONLY: return TOP_LOCAL_SET_PATH; + default: throw new IllegalArgumentException(); + } + } + + // open for debug + void addSource(MediaSource source) { + mSourceMap.put(source.getPrefix(), source); + } + + public MediaObject peekMediaObject(Path path) { + return path.getObject(); + } + + public MediaSet peekMediaSet(Path path) { + return (MediaSet) path.getObject(); + } + + public MediaObject getMediaObject(Path path) { + MediaObject obj = path.getObject(); + if (obj != null) return obj; + + MediaSource source = mSourceMap.get(path.getPrefix()); + if (source == null) { + Log.w(TAG, "cannot find media source for path: " + path); + return null; + } + + MediaObject object = source.createMediaObject(path); + if (object == null) { + Log.w(TAG, "cannot create media object: " + path); + } + return object; + } + + public MediaObject getMediaObject(String s) { + return getMediaObject(Path.fromString(s)); + } + + public MediaSet getMediaSet(Path path) { + return (MediaSet) getMediaObject(path); + } + + public MediaSet getMediaSet(String s) { + return (MediaSet) getMediaObject(s); + } + + public MediaSet[] getMediaSetsFromString(String segment) { + String[] seq = Path.splitSequence(segment); + int n = seq.length; + MediaSet[] sets = new MediaSet[n]; + for (int i = 0; i < n; i++) { + sets[i] = getMediaSet(seq[i]); + } + return sets; + } + + // Maps a list of Paths to MediaItems, and invoke consumer.consume() + // for each MediaItem (may not be in the same order as the input list). + // An index number is also passed to consumer.consume() to identify + // the original position in the input list of the corresponding Path (plus + // startIndex). + public void mapMediaItems(ArrayList list, ItemConsumer consumer, + int startIndex) { + HashMap> map = + new HashMap>(); + + // Group the path by the prefix. + int n = list.size(); + for (int i = 0; i < n; i++) { + Path path = list.get(i); + String prefix = path.getPrefix(); + ArrayList group = map.get(prefix); + if (group == null) { + group = new ArrayList(); + map.put(prefix, group); + } + group.add(new PathId(path, i + startIndex)); + } + + // For each group, ask the corresponding media source to map it. + for (Entry> entry : map.entrySet()) { + String prefix = entry.getKey(); + MediaSource source = mSourceMap.get(prefix); + source.mapMediaItems(entry.getValue(), consumer); + } + } + + // The following methods forward the request to the proper object. + public int getSupportedOperations(Path path) { + return getMediaObject(path).getSupportedOperations(); + } + + public void delete(Path path) { + getMediaObject(path).delete(); + } + + public void rotate(Path path, int degrees) { + getMediaObject(path).rotate(degrees); + } + + public Uri getContentUri(Path path) { + return getMediaObject(path).getContentUri(); + } + + public int getMediaType(Path path) { + return getMediaObject(path).getMediaType(); + } + + public MediaDetails getDetails(Path path) { + return getMediaObject(path).getDetails(); + } + + public void cache(Path path, int flag) { + getMediaObject(path).cache(flag); + } + + public Path findPathByUri(Uri uri) { + if (uri == null) return null; + for (MediaSource source : mSourceMap.values()) { + Path path = source.findPathByUri(uri); + if (path != null) return path; + } + return null; + } + + public Path getDefaultSetOf(Path item) { + MediaSource source = mSourceMap.get(item.getPrefix()); + return source == null ? null : source.getDefaultSetOf(item); + } + + // Returns number of bytes used by cached pictures currently downloaded. + public long getTotalUsedCacheSize() { + long sum = 0; + for (MediaSource source : mSourceMap.values()) { + sum += source.getTotalUsedCacheSize(); + } + return sum; + } + + // Returns number of bytes used by cached pictures if all pending + // downloads and removals are completed. + public long getTotalTargetCacheSize() { + long sum = 0; + for (MediaSource source : mSourceMap.values()) { + sum += source.getTotalTargetCacheSize(); + } + return sum; + } + + public void registerChangeNotifier(Uri uri, ChangeNotifier notifier) { + NotifyBroker broker = null; + synchronized (mNotifierMap) { + broker = mNotifierMap.get(uri); + if (broker == null) { + broker = new NotifyBroker(mDefaultMainHandler); + mApplication.getContentResolver() + .registerContentObserver(uri, true, broker); + mNotifierMap.put(uri, broker); + } + } + broker.registerNotifier(notifier); + } + + public void resume() { + if (++mActiveCount == 1) { + for (MediaSource source : mSourceMap.values()) { + source.resume(); + } + } + } + + public void pause() { + if (--mActiveCount == 0) { + for (MediaSource source : mSourceMap.values()) { + source.pause(); + } + } + } + + private static class NotifyBroker extends ContentObserver { + private WeakHashMap mNotifiers = + new WeakHashMap(); + + public NotifyBroker(Handler handler) { + super(handler); + } + + public synchronized void registerNotifier(ChangeNotifier notifier) { + mNotifiers.put(notifier, null); + } + + @Override + public synchronized void onChange(boolean selfChange) { + for(ChangeNotifier notifier : mNotifiers.keySet()) { + notifier.onChange(selfChange); + } + } + } +} diff --git a/src/com/android/gallery3d/data/DecodeUtils.java b/src/com/android/gallery3d/data/DecodeUtils.java new file mode 100644 index 000000000..e7ae638c2 --- /dev/null +++ b/src/com/android/gallery3d/data/DecodeUtils.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.content.ContentResolver; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.BitmapFactory.Options; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Rect; +import android.net.Uri; +import android.os.ParcelFileDescriptor; + +import java.io.FileDescriptor; +import java.io.FileInputStream; + +public class DecodeUtils { + private static final String TAG = "DecodeService"; + + private static class DecodeCanceller implements CancelListener { + Options mOptions; + public DecodeCanceller(Options options) { + mOptions = options; + } + public void onCancel() { + mOptions.requestCancelDecode(); + } + } + + public static Bitmap requestDecode(JobContext jc, final String filePath, + Options options) { + if (options == null) options = new Options(); + jc.setCancelListener(new DecodeCanceller(options)); + return ensureGLCompatibleBitmap( + BitmapFactory.decodeFile(filePath, options)); + } + + public static Bitmap requestDecode(JobContext jc, byte[] bytes, + Options options) { + return requestDecode(jc, bytes, 0, bytes.length, options); + } + + public static Bitmap requestDecode(JobContext jc, byte[] bytes, int offset, + int length, Options options) { + if (options == null) options = new Options(); + jc.setCancelListener(new DecodeCanceller(options)); + return ensureGLCompatibleBitmap( + BitmapFactory.decodeByteArray(bytes, offset, length, options)); + } + + public static Bitmap requestDecode(JobContext jc, final String filePath, + Options options, int targetSize) { + FileInputStream fis = null; + try { + fis = new FileInputStream(filePath); + FileDescriptor fd = fis.getFD(); + return requestDecode(jc, fd, options, targetSize); + } catch (Exception ex) { + Log.w(TAG, ex); + return null; + } finally { + Utils.closeSilently(fis); + } + } + + public static Bitmap requestDecode(JobContext jc, FileDescriptor fd, + Options options, int targetSize) { + if (options == null) options = new Options(); + jc.setCancelListener(new DecodeCanceller(options)); + + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor(fd, null, options); + if (jc.isCancelled()) return null; + + options.inSampleSize = BitmapUtils.computeSampleSizeLarger( + options.outWidth, options.outHeight, targetSize); + options.inJustDecodeBounds = false; + return ensureGLCompatibleBitmap( + BitmapFactory.decodeFileDescriptor(fd, null, options)); + } + + public static Bitmap requestDecode(JobContext jc, + FileDescriptor fileDescriptor, Rect paddings, Options options) { + if (options == null) options = new Options(); + jc.setCancelListener(new DecodeCanceller(options)); + return ensureGLCompatibleBitmap(BitmapFactory.decodeFileDescriptor + (fileDescriptor, paddings, options)); + } + + // TODO: This function should not be called directly from + // DecodeUtils.requestDecode(...), since we don't have the knowledge + // if the bitmap will be uploaded to GL. + public static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) { + if (bitmap == null || bitmap.getConfig() != null) return bitmap; + Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false); + bitmap.recycle(); + return newBitmap; + } + + public static BitmapRegionDecoder requestCreateBitmapRegionDecoder( + JobContext jc, byte[] bytes, int offset, int length, + boolean shareable) { + if (offset < 0 || length <= 0 || offset + length > bytes.length) { + throw new IllegalArgumentException(String.format( + "offset = %s, length = %s, bytes = %s", + offset, length, bytes.length)); + } + + try { + return BitmapRegionDecoder.newInstance( + bytes, offset, length, shareable); + } catch (Throwable t) { + Log.w(TAG, t); + return null; + } + } + + public static BitmapRegionDecoder requestCreateBitmapRegionDecoder( + JobContext jc, String filePath, boolean shareable) { + try { + return BitmapRegionDecoder.newInstance(filePath, shareable); + } catch (Throwable t) { + Log.w(TAG, t); + return null; + } + } + + public static BitmapRegionDecoder requestCreateBitmapRegionDecoder( + JobContext jc, FileDescriptor fd, boolean shareable) { + try { + return BitmapRegionDecoder.newInstance(fd, shareable); + } catch (Throwable t) { + Log.w(TAG, t); + return null; + } + } + + public static BitmapRegionDecoder requestCreateBitmapRegionDecoder( + JobContext jc, Uri uri, ContentResolver resolver, + boolean shareable) { + ParcelFileDescriptor pfd = null; + try { + pfd = resolver.openFileDescriptor(uri, "r"); + return BitmapRegionDecoder.newInstance( + pfd.getFileDescriptor(), shareable); + } catch (Throwable t) { + Log.w(TAG, t); + return null; + } finally { + Utils.closeSilently(pfd); + } + } +} diff --git a/src/com/android/gallery3d/data/DownloadCache.java b/src/com/android/gallery3d/data/DownloadCache.java new file mode 100644 index 000000000..30ba668c3 --- /dev/null +++ b/src/com/android/gallery3d/data/DownloadCache.java @@ -0,0 +1,398 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.LruCache; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DownloadEntry.Columns; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import java.io.File; +import java.net.URL; +import java.util.HashMap; +import java.util.HashSet; +import java.util.WeakHashMap; + +public class DownloadCache { + private static final String TAG = "DownloadCache"; + private static final int MAX_DELETE_COUNT = 16; + private static final int LRU_CAPACITY = 4; + + private static final String TABLE_NAME = DownloadEntry.SCHEMA.getTableName(); + + private static final String QUERY_PROJECTION[] = {Columns.ID, Columns.DATA}; + private static final String WHERE_HASH_AND_URL = String.format( + "%s = ? AND %s = ?", Columns.HASH_CODE, Columns.CONTENT_URL); + private static final int QUERY_INDEX_ID = 0; + private static final int QUERY_INDEX_DATA = 1; + + private static final String FREESPACE_PROJECTION[] = { + Columns.ID, Columns.DATA, Columns.CONTENT_URL, Columns.CONTENT_SIZE}; + private static final String FREESPACE_ORDER_BY = + String.format("%s ASC", Columns.LAST_ACCESS); + private static final int FREESPACE_IDNEX_ID = 0; + private static final int FREESPACE_IDNEX_DATA = 1; + private static final int FREESPACE_INDEX_CONTENT_URL = 2; + private static final int FREESPACE_INDEX_CONTENT_SIZE = 3; + + private static final String ID_WHERE = Columns.ID + " = ?"; + + private static final String SUM_PROJECTION[] = + {String.format("sum(%s)", Columns.CONTENT_SIZE)}; + private static final int SUM_INDEX_SUM = 0; + + private final LruCache mEntryMap = + new LruCache(LRU_CAPACITY); + private final HashMap mTaskMap = + new HashMap(); + private final File mRoot; + private final GalleryApp mApplication; + private final SQLiteDatabase mDatabase; + private final long mCapacity; + + private long mTotalBytes = 0; + private boolean mInitialized = false; + private WeakHashMap mAssociateMap = new WeakHashMap(); + + public DownloadCache(GalleryApp application, File root, long capacity) { + mRoot = Utils.checkNotNull(root); + mApplication = Utils.checkNotNull(application); + mCapacity = capacity; + mDatabase = new DatabaseHelper(application.getAndroidContext()) + .getWritableDatabase(); + } + + private Entry findEntryInDatabase(String stringUrl) { + long hash = Utils.crc64Long(stringUrl); + String whereArgs[] = {String.valueOf(hash), stringUrl}; + Cursor cursor = mDatabase.query(TABLE_NAME, QUERY_PROJECTION, + WHERE_HASH_AND_URL, whereArgs, null, null, null); + try { + if (cursor.moveToNext()) { + File file = new File(cursor.getString(QUERY_INDEX_DATA)); + long id = cursor.getInt(QUERY_INDEX_ID); + Entry entry = null; + synchronized (mEntryMap) { + entry = mEntryMap.get(stringUrl); + if (entry == null) { + entry = new Entry(id, file); + mEntryMap.put(stringUrl, entry); + } + } + return entry; + } + } finally { + cursor.close(); + } + return null; + } + + public Entry lookup(URL url) { + if (!mInitialized) initialize(); + String stringUrl = url.toString(); + + // First find in the entry-pool + synchronized (mEntryMap) { + Entry entry = mEntryMap.get(stringUrl); + if (entry != null) { + updateLastAccess(entry.mId); + return entry; + } + } + + // Then, find it in database + TaskProxy proxy = new TaskProxy(); + synchronized (mTaskMap) { + Entry entry = findEntryInDatabase(stringUrl); + if (entry != null) { + updateLastAccess(entry.mId); + return entry; + } + } + return null; + } + + public Entry download(JobContext jc, URL url) { + if (!mInitialized) initialize(); + + String stringUrl = url.toString(); + + // First find in the entry-pool + synchronized (mEntryMap) { + Entry entry = mEntryMap.get(stringUrl); + if (entry != null) { + updateLastAccess(entry.mId); + return entry; + } + } + + // Then, find it in database + TaskProxy proxy = new TaskProxy(); + synchronized (mTaskMap) { + Entry entry = findEntryInDatabase(stringUrl); + if (entry != null) { + updateLastAccess(entry.mId); + return entry; + } + + // Finally, we need to download the file .... + // First check if we are downloading it now ... + DownloadTask task = mTaskMap.get(stringUrl); + if (task == null) { // if not, start the download task now + task = new DownloadTask(stringUrl); + mTaskMap.put(stringUrl, task); + task.mFuture = mApplication.getThreadPool().submit(task, task); + } + task.addProxy(proxy); + } + + return proxy.get(jc); + } + + private void updateLastAccess(long id) { + ContentValues values = new ContentValues(); + values.put(Columns.LAST_ACCESS, System.currentTimeMillis()); + mDatabase.update(TABLE_NAME, values, + ID_WHERE, new String[] {String.valueOf(id)}); + } + + private synchronized void freeSomeSpaceIfNeed(int maxDeleteFileCount) { + if (mTotalBytes <= mCapacity) return; + Cursor cursor = mDatabase.query(TABLE_NAME, + FREESPACE_PROJECTION, null, null, null, null, FREESPACE_ORDER_BY); + try { + while (maxDeleteFileCount > 0 + && mTotalBytes > mCapacity && cursor.moveToNext()) { + long id = cursor.getLong(FREESPACE_IDNEX_ID); + String url = cursor.getString(FREESPACE_INDEX_CONTENT_URL); + long size = cursor.getLong(FREESPACE_INDEX_CONTENT_SIZE); + String path = cursor.getString(FREESPACE_IDNEX_DATA); + boolean containsKey; + synchronized (mEntryMap) { + containsKey = mEntryMap.containsKey(url); + } + if (!containsKey) { + --maxDeleteFileCount; + mTotalBytes -= size; + new File(path).delete(); + mDatabase.delete(TABLE_NAME, + ID_WHERE, new String[]{String.valueOf(id)}); + } else { + // skip delete, since it is being used + } + } + } finally { + cursor.close(); + } + } + + private synchronized long insertEntry(String url, File file) { + long size = file.length(); + mTotalBytes += size; + + ContentValues values = new ContentValues(); + String hashCode = String.valueOf(Utils.crc64Long(url)); + values.put(Columns.DATA, file.getAbsolutePath()); + values.put(Columns.HASH_CODE, hashCode); + values.put(Columns.CONTENT_URL, url); + values.put(Columns.CONTENT_SIZE, size); + values.put(Columns.LAST_UPDATED, System.currentTimeMillis()); + return mDatabase.insert(TABLE_NAME, "", values); + } + + private synchronized void initialize() { + if (mInitialized) return; + mInitialized = true; + if (!mRoot.isDirectory()) mRoot.mkdirs(); + if (!mRoot.isDirectory()) { + throw new RuntimeException("cannot create " + mRoot.getAbsolutePath()); + } + + Cursor cursor = mDatabase.query( + TABLE_NAME, SUM_PROJECTION, null, null, null, null, null); + mTotalBytes = 0; + try { + if (cursor.moveToNext()) { + mTotalBytes = cursor.getLong(SUM_INDEX_SUM); + } + } finally { + cursor.close(); + } + if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT); + } + + private final class DatabaseHelper extends SQLiteOpenHelper { + public static final String DATABASE_NAME = "download.db"; + public static final int DATABASE_VERSION = 2; + + public DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + DownloadEntry.SCHEMA.createTables(db); + // Delete old files + for (File file : mRoot.listFiles()) { + if (!file.delete()) { + Log.w(TAG, "fail to remove: " + file.getAbsolutePath()); + } + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + //reset everything + DownloadEntry.SCHEMA.dropTables(db); + onCreate(db); + } + } + + public class Entry { + public File cacheFile; + protected long mId; + + Entry(long id, File cacheFile) { + mId = id; + this.cacheFile = Utils.checkNotNull(cacheFile); + } + + public void associateWith(Object object) { + mAssociateMap.put(Utils.checkNotNull(object), this); + } + } + + private class DownloadTask implements Job, FutureListener { + private HashSet mProxySet = new HashSet(); + private Future mFuture; + private final String mUrl; + + public DownloadTask(String url) { + mUrl = Utils.checkNotNull(url); + } + + public void removeProxy(TaskProxy proxy) { + synchronized (mTaskMap) { + Utils.assertTrue(mProxySet.remove(proxy)); + if (mProxySet.isEmpty()) { + mFuture.cancel(); + mTaskMap.remove(mUrl); + } + } + } + + // should be used in synchronized block of mDatabase + public void addProxy(TaskProxy proxy) { + proxy.mTask = this; + mProxySet.add(proxy); + } + + public void onFutureDone(Future future) { + File file = future.get(); + long id = 0; + if (file != null) { // insert to database + id = insertEntry(mUrl, file); + } + + if (future.isCancelled()) { + Utils.assertTrue(mProxySet.isEmpty()); + return; + } + + synchronized (mTaskMap) { + Entry entry = null; + synchronized (mEntryMap) { + if (file != null) { + entry = new Entry(id, file); + Utils.assertTrue(mEntryMap.put(mUrl, entry) == null); + } + } + for (TaskProxy proxy : mProxySet) { + proxy.setResult(entry); + } + mTaskMap.remove(mUrl); + freeSomeSpaceIfNeed(MAX_DELETE_COUNT); + } + } + + public File run(JobContext jc) { + // TODO: utilize etag + jc.setMode(ThreadPool.MODE_NETWORK); + File tempFile = null; + try { + URL url = new URL(mUrl); + tempFile = File.createTempFile("cache", ".tmp", mRoot); + // download from url to tempFile + jc.setMode(ThreadPool.MODE_NETWORK); + boolean downloaded = DownloadUtils.requestDownload(jc, url, tempFile); + jc.setMode(ThreadPool.MODE_NONE); + if (downloaded) return tempFile; + } catch (Exception e) { + Log.e(TAG, String.format("fail to download %s", mUrl), e); + } finally { + jc.setMode(ThreadPool.MODE_NONE); + } + if (tempFile != null) tempFile.delete(); + return null; + } + } + + public static class TaskProxy { + private DownloadTask mTask; + private boolean mIsCancelled = false; + private Entry mEntry; + + synchronized void setResult(Entry entry) { + if (mIsCancelled) return; + mEntry = entry; + notifyAll(); + } + + public synchronized Entry get(JobContext jc) { + jc.setCancelListener(new CancelListener() { + public void onCancel() { + mTask.removeProxy(TaskProxy.this); + synchronized (TaskProxy.this) { + mIsCancelled = true; + TaskProxy.this.notifyAll(); + } + } + }); + while (!mIsCancelled && mEntry == null) { + try { + wait(); + } catch (InterruptedException e) { + Log.w(TAG, "ignore interrupt", e); + } + } + jc.setCancelListener(null); + return mEntry; + } + } +} diff --git a/src/com/android/gallery3d/data/DownloadEntry.java b/src/com/android/gallery3d/data/DownloadEntry.java new file mode 100644 index 000000000..578523f73 --- /dev/null +++ b/src/com/android/gallery3d/data/DownloadEntry.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.gallery3d.data; + +import com.android.gallery3d.common.Entry; +import com.android.gallery3d.common.EntrySchema; + + +@Entry.Table("download") +public class DownloadEntry extends Entry { + public static final EntrySchema SCHEMA = new EntrySchema(DownloadEntry.class); + + public static interface Columns extends Entry.Columns { + public static final String HASH_CODE = "hash_code"; + public static final String CONTENT_URL = "content_url"; + public static final String CONTENT_SIZE = "_size"; + public static final String ETAG = "etag"; + public static final String LAST_ACCESS = "last_access"; + public static final String LAST_UPDATED = "last_updated"; + public static final String DATA = "_data"; + } + + @Column(value = "hash_code", indexed = true) + public long hashCode; + + @Column("content_url") + public String contentUrl; + + @Column("_size") + public long contentSize; + + @Column("etag") + public String eTag; + + @Column(value = "last_access", indexed = true) + public long lastAccessTime; + + @Column(value = "last_updated") + public long lastUpdatedTime; + + @Column("_data") + public String path; + + @Override + public String toString() { + // Note: THIS IS REQUIRED. We used all the fields here. Otherwise, + // ProGuard will remove these UNUSED fields. However, these + // fields are needed to generate database. + return new StringBuilder() + .append("hash_code: ").append(hashCode).append(", ") + .append("content_url").append(contentUrl).append(", ") + .append("_size").append(contentSize).append(", ") + .append("etag").append(eTag).append(", ") + .append("last_access").append(lastAccessTime).append(", ") + .append("last_updated").append(lastUpdatedTime).append(",") + .append("_data").append(path) + .toString(); + } +} diff --git a/src/com/android/gallery3d/data/DownloadUtils.java b/src/com/android/gallery3d/data/DownloadUtils.java new file mode 100644 index 000000000..9632db984 --- /dev/null +++ b/src/com/android/gallery3d/data/DownloadUtils.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.net.URL; + +public class DownloadUtils { + private static final String TAG = "DownloadService"; + + public static boolean requestDownload(JobContext jc, URL url, File file) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file); + return download(jc, url, fos); + } catch (Throwable t) { + return false; + } finally { + Utils.closeSilently(fos); + } + } + + public static byte[] requestDownload(JobContext jc, URL url) { + ByteArrayOutputStream baos = null; + try { + baos = new ByteArrayOutputStream(); + if (!download(jc, url, baos)) { + return null; + } + return baos.toByteArray(); + } catch (Throwable t) { + Log.w(TAG, t); + return null; + } finally { + Utils.closeSilently(baos); + } + } + + public static void dump(JobContext jc, InputStream is, OutputStream os) + throws IOException { + byte buffer[] = new byte[4096]; + int rc = is.read(buffer, 0, buffer.length); + final Thread thread = Thread.currentThread(); + jc.setCancelListener(new CancelListener() { + public void onCancel() { + thread.interrupt(); + } + }); + while (rc > 0) { + if (jc.isCancelled()) throw new InterruptedIOException(); + os.write(buffer, 0, rc); + rc = is.read(buffer, 0, buffer.length); + } + jc.setCancelListener(null); + Thread.interrupted(); // consume the interrupt signal + } + + public static boolean download(JobContext jc, URL url, OutputStream output) { + InputStream input = null; + try { + input = url.openStream(); + dump(jc, input, output); + return true; + } catch (Throwable t) { + Log.w(TAG, "fail to download", t); + return false; + } finally { + Utils.closeSilently(input); + } + } +} \ No newline at end of file diff --git a/src/com/android/gallery3d/data/Face.java b/src/com/android/gallery3d/data/Face.java new file mode 100644 index 000000000..cc1a2d3dc --- /dev/null +++ b/src/com/android/gallery3d/data/Face.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.common.Utils; + +public class Face implements Comparable { + private String mName; + private String mPersonId; + + public Face(String name, String personId) { + mName = name; + mPersonId = personId; + Utils.assertTrue(mName != null && mPersonId != null); + } + + public String getName() { + return mName; + } + + public String getPersonId() { + return mPersonId; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Face) { + Face face = (Face) obj; + return mPersonId.equals(face.mPersonId); + } + return false; + } + + @Override + public int hashCode() { + return mPersonId.hashCode(); + } + + public int compareTo(Face another) { + return mPersonId.compareTo(another.mPersonId); + } +} diff --git a/src/com/android/gallery3d/data/FaceClustering.java b/src/com/android/gallery3d/data/FaceClustering.java new file mode 100644 index 000000000..6ed73b560 --- /dev/null +++ b/src/com/android/gallery3d/data/FaceClustering.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.R; + +import android.content.Context; + +import java.util.ArrayList; +import java.util.Map; +import java.util.TreeMap; + +public class FaceClustering extends Clustering { + @SuppressWarnings("unused") + private static final String TAG = "FaceClustering"; + + private ArrayList> mClusters; + private String[] mNames; + private String mUntaggedString; + + public FaceClustering(Context context) { + mUntaggedString = context.getResources().getString(R.string.untagged); + } + + @Override + public void run(MediaSet baseSet) { + final TreeMap> map = + new TreeMap>(); + final ArrayList untagged = new ArrayList(); + + baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + public void consume(int index, MediaItem item) { + Path path = item.getPath(); + + Face[] faces = item.getFaces(); + if (faces == null || faces.length == 0) { + untagged.add(path); + return; + } + for (int j = 0; j < faces.length; j++) { + Face key = faces[j]; + ArrayList list = map.get(key); + if (list == null) { + list = new ArrayList(); + map.put(key, list); + } + list.add(path); + } + } + }); + + int m = map.size(); + mClusters = new ArrayList>(); + mNames = new String[m + ((untagged.size() > 0) ? 1 : 0)]; + int i = 0; + for (Map.Entry> entry : map.entrySet()) { + mNames[i++] = entry.getKey().getName(); + mClusters.add(entry.getValue()); + } + if (untagged.size() > 0) { + mNames[i++] = mUntaggedString; + mClusters.add(untagged); + } + } + + @Override + public int getNumberOfClusters() { + return mClusters.size(); + } + + @Override + public ArrayList getCluster(int index) { + return mClusters.get(index); + } + + @Override + public String getClusterName(int index) { + return mNames[index]; + } +} diff --git a/src/com/android/gallery3d/data/FilterSet.java b/src/com/android/gallery3d/data/FilterSet.java new file mode 100644 index 000000000..9cb7e02ef --- /dev/null +++ b/src/com/android/gallery3d/data/FilterSet.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import java.util.ArrayList; + +// FilterSet filters a base MediaSet according to a condition. Currently the +// condition is a matching media type. It can be extended to other conditions +// if needed. +public class FilterSet extends MediaSet implements ContentListener { + @SuppressWarnings("unused") + private static final String TAG = "FilterSet"; + + private final DataManager mDataManager; + private final MediaSet mBaseSet; + private final int mMediaType; + private final ArrayList mPaths = new ArrayList(); + private final ArrayList mAlbums = new ArrayList(); + + public FilterSet(Path path, DataManager dataManager, MediaSet baseSet, + int mediaType) { + super(path, INVALID_DATA_VERSION); + mDataManager = dataManager; + mBaseSet = baseSet; + mMediaType = mediaType; + mBaseSet.addContentListener(this); + } + + @Override + public String getName() { + return mBaseSet.getName(); + } + + @Override + public MediaSet getSubMediaSet(int index) { + return mAlbums.get(index); + } + + @Override + public int getSubMediaSetCount() { + return mAlbums.size(); + } + + @Override + public int getMediaItemCount() { + return mPaths.size(); + } + + @Override + public ArrayList getMediaItem(int start, int count) { + return ClusterAlbum.getMediaItemFromPath( + mPaths, start, count, mDataManager); + } + + @Override + public long reload() { + if (mBaseSet.reload() > mDataVersion) { + updateData(); + mDataVersion = nextVersionNumber(); + } + return mDataVersion; + } + + @Override + public void onContentDirty() { + notifyContentChanged(); + } + + private void updateData() { + // Albums + mAlbums.clear(); + String basePath = "/filter/mediatype/" + mMediaType; + + for (int i = 0, n = mBaseSet.getSubMediaSetCount(); i < n; i++) { + MediaSet set = mBaseSet.getSubMediaSet(i); + String filteredPath = basePath + "/{" + set.getPath().toString() + "}"; + MediaSet filteredSet = mDataManager.getMediaSet(filteredPath); + filteredSet.reload(); + if (filteredSet.getMediaItemCount() > 0 + || filteredSet.getSubMediaSetCount() > 0) { + mAlbums.add(filteredSet); + } + } + + // Items + mPaths.clear(); + final int total = mBaseSet.getMediaItemCount(); + final Path[] buf = new Path[total]; + + mBaseSet.enumerateMediaItems(new MediaSet.ItemConsumer() { + public void consume(int index, MediaItem item) { + if (item.getMediaType() == mMediaType) { + if (index < 0 || index >= total) return; + Path path = item.getPath(); + buf[index] = path; + } + } + }); + + for (int i = 0; i < total; i++) { + if (buf[i] != null) { + mPaths.add(buf[i]); + } + } + } + + @Override + public int getSupportedOperations() { + return SUPPORT_SHARE | SUPPORT_DELETE; + } + + @Override + public void delete() { + ItemConsumer consumer = new ItemConsumer() { + public void consume(int index, MediaItem item) { + if ((item.getSupportedOperations() & SUPPORT_DELETE) != 0) { + item.delete(); + } + } + }; + mDataManager.mapMediaItems(mPaths, consumer, 0); + } +} diff --git a/src/com/android/gallery3d/data/FilterSource.java b/src/com/android/gallery3d/data/FilterSource.java new file mode 100644 index 000000000..d1a04c995 --- /dev/null +++ b/src/com/android/gallery3d/data/FilterSource.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.GalleryApp; + +class FilterSource extends MediaSource { + private static final String TAG = "FilterSource"; + private static final int FILTER_BY_MEDIATYPE = 0; + + private GalleryApp mApplication; + private PathMatcher mMatcher; + + public FilterSource(GalleryApp application) { + super("filter"); + mApplication = application; + mMatcher = new PathMatcher(); + mMatcher.add("/filter/mediatype/*/*", FILTER_BY_MEDIATYPE); + } + + // The name we accept is: + // /filter/mediatype/k/{set} + // where k is the media type we want. + @Override + public MediaObject createMediaObject(Path path) { + int matchType = mMatcher.match(path); + int mediaType = mMatcher.getIntVar(0); + String setsName = mMatcher.getVar(1); + DataManager dataManager = mApplication.getDataManager(); + MediaSet[] sets = dataManager.getMediaSetsFromString(setsName); + switch (matchType) { + case FILTER_BY_MEDIATYPE: + return new FilterSet(path, dataManager, sets[0], mediaType); + default: + throw new RuntimeException("bad path: " + path); + } + } +} diff --git a/src/com/android/gallery3d/data/ImageCacheRequest.java b/src/com/android/gallery3d/data/ImageCacheRequest.java new file mode 100644 index 000000000..104ff4839 --- /dev/null +++ b/src/com/android/gallery3d/data/ImageCacheRequest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.data.ImageCacheService.ImageData; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +abstract class ImageCacheRequest implements Job { + private static final String TAG = "ImageCacheRequest"; + + protected GalleryApp mApplication; + private Path mPath; + private int mType; + private int mTargetSize; + + public ImageCacheRequest(GalleryApp application, + Path path, int type, int targetSize) { + mApplication = application; + mPath = path; + mType = type; + mTargetSize = targetSize; + } + + public Bitmap run(JobContext jc) { + String debugTag = mPath + "," + + ((mType == MediaItem.TYPE_THUMBNAIL) ? "THUMB" : + (mType == MediaItem.TYPE_MICROTHUMBNAIL) ? "MICROTHUMB" : "?"); + ImageCacheService cacheService = mApplication.getImageCacheService(); + + ImageData data = cacheService.getImageData(mPath, mType); + if (jc.isCancelled()) return null; + + if (data != null) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap bitmap = DecodeUtils.requestDecode(jc, data.mData, + data.mOffset, data.mData.length - data.mOffset, options); + if (bitmap == null && !jc.isCancelled()) { + Log.w(TAG, "decode cached failed " + debugTag); + } + return bitmap; + } else { + Bitmap bitmap = onDecodeOriginal(jc, mType); + if (jc.isCancelled()) return null; + + if (bitmap == null) { + Log.w(TAG, "decode orig failed " + debugTag); + return null; + } + + if (mType == MediaItem.TYPE_MICROTHUMBNAIL) { + bitmap = BitmapUtils.resizeDownAndCropCenter(bitmap, + mTargetSize, true); + } else { + bitmap = BitmapUtils.resizeDownBySideLength(bitmap, + mTargetSize, true); + } + if (jc.isCancelled()) return null; + + byte[] array = BitmapUtils.compressBitmap(bitmap); + if (jc.isCancelled()) return null; + + cacheService.putImageData(mPath, mType, array); + return bitmap; + } + } + + public abstract Bitmap onDecodeOriginal(JobContext jc, int targetSize); +} diff --git a/src/com/android/gallery3d/data/ImageCacheService.java b/src/com/android/gallery3d/data/ImageCacheService.java new file mode 100644 index 000000000..3adce1332 --- /dev/null +++ b/src/com/android/gallery3d/data/ImageCacheService.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.common.BlobCache; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.CacheManager; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.Context; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class ImageCacheService { + @SuppressWarnings("unused") + private static final String TAG = "ImageCacheService"; + + private static final String IMAGE_CACHE_FILE = "imgcache"; + private static final int IMAGE_CACHE_MAX_ENTRIES = 5000; + private static final int IMAGE_CACHE_MAX_BYTES = 200 * 1024 * 1024; + private static final int IMAGE_CACHE_VERSION = 3; + + private BlobCache mCache; + + public ImageCacheService(Context context) { + mCache = CacheManager.getCache(context, IMAGE_CACHE_FILE, + IMAGE_CACHE_MAX_ENTRIES, IMAGE_CACHE_MAX_BYTES, + IMAGE_CACHE_VERSION); + } + + public static class ImageData { + public ImageData(byte[] data, int offset) { + mData = data; + mOffset = offset; + } + public byte[] mData; + public int mOffset; + } + + public ImageData getImageData(Path path, int type) { + byte[] key = makeKey(path, type); + long cacheKey = Utils.crc64Long(key); + try { + byte[] value = null; + synchronized (mCache) { + value = mCache.lookup(cacheKey); + } + if (value == null) return null; + if (isSameKey(key, value)) { + int offset = key.length; + return new ImageData(value, offset); + } + } catch (IOException ex) { + // ignore. + } + return null; + } + + public void putImageData(Path path, int type, byte[] value) { + byte[] key = makeKey(path, type); + long cacheKey = Utils.crc64Long(key); + ByteBuffer buffer = ByteBuffer.allocate(key.length + value.length); + buffer.put(key); + buffer.put(value); + synchronized (mCache) { + try { + mCache.insert(cacheKey, buffer.array()); + } catch (IOException ex) { + // ignore. + } + } + } + + private static byte[] makeKey(Path path, int type) { + return GalleryUtils.getBytes(path.toString() + "+" + type); + } + + private static boolean isSameKey(byte[] key, byte[] buffer) { + int n = key.length; + if (buffer.length < n) { + return false; + } + for (int i = 0; i < n; ++i) { + if (key[i] != buffer[i]) { + return false; + } + } + return true; + } +} diff --git a/src/com/android/gallery3d/data/LocalAlbum.java b/src/com/android/gallery3d/data/LocalAlbum.java new file mode 100644 index 000000000..5bd4398b4 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalAlbum.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.provider.MediaStore.Video; +import android.provider.MediaStore.Video.VideoColumns; + +import java.util.ArrayList; + +// LocalAlbumSet lists all media items in one bucket on local storage. +// The media items need to be all images or all videos, but not both. +public class LocalAlbum extends MediaSet { + private static final String TAG = "LocalAlbum"; + private static final String[] COUNT_PROJECTION = { "count(*)" }; + + private static final int INVALID_COUNT = -1; + private final String mWhereClause; + private final String mOrderClause; + private final Uri mBaseUri; + private final String[] mProjection; + + private final GalleryApp mApplication; + private final ContentResolver mResolver; + private final int mBucketId; + private final String mBucketName; + private final boolean mIsImage; + private final ChangeNotifier mNotifier; + private final Path mItemPath; + private int mCachedCount = INVALID_COUNT; + + public LocalAlbum(Path path, GalleryApp application, int bucketId, + boolean isImage, String name) { + super(path, nextVersionNumber()); + mApplication = application; + mResolver = application.getContentResolver(); + mBucketId = bucketId; + mBucketName = name; + mIsImage = isImage; + + if (isImage) { + mWhereClause = ImageColumns.BUCKET_ID + " = ?"; + mOrderClause = ImageColumns.DATE_TAKEN + " DESC, " + + ImageColumns._ID + " DESC"; + mBaseUri = Images.Media.EXTERNAL_CONTENT_URI; + mProjection = LocalImage.PROJECTION; + mItemPath = LocalImage.ITEM_PATH; + } else { + mWhereClause = VideoColumns.BUCKET_ID + " = ?"; + mOrderClause = VideoColumns.DATE_TAKEN + " DESC, " + + VideoColumns._ID + " DESC"; + mBaseUri = Video.Media.EXTERNAL_CONTENT_URI; + mProjection = LocalVideo.PROJECTION; + mItemPath = LocalVideo.ITEM_PATH; + } + + mNotifier = new ChangeNotifier(this, mBaseUri, application); + } + + public LocalAlbum(Path path, GalleryApp application, int bucketId, + boolean isImage) { + this(path, application, bucketId, isImage, + LocalAlbumSet.getBucketName(application.getContentResolver(), + bucketId)); + } + + @Override + public ArrayList getMediaItem(int start, int count) { + DataManager dataManager = mApplication.getDataManager(); + Uri uri = mBaseUri.buildUpon() + .appendQueryParameter("limit", start + "," + count).build(); + ArrayList list = new ArrayList(); + GalleryUtils.assertNotInRenderThread(); + Cursor cursor = mResolver.query( + uri, mProjection, mWhereClause, + new String[]{String.valueOf(mBucketId)}, + mOrderClause); + if (cursor == null) { + Log.w(TAG, "query fail: " + uri); + return list; + } + + try { + while (cursor.moveToNext()) { + int id = cursor.getInt(0); // _id must be in the first column + Path childPath = mItemPath.getChild(id); + MediaItem item = loadOrUpdateItem(childPath, cursor, + dataManager, mApplication, mIsImage); + list.add(item); + } + } finally { + cursor.close(); + } + return list; + } + + private static MediaItem loadOrUpdateItem(Path path, Cursor cursor, + DataManager dataManager, GalleryApp app, boolean isImage) { + LocalMediaItem item = (LocalMediaItem) dataManager.peekMediaObject(path); + if (item == null) { + if (isImage) { + item = new LocalImage(path, app, cursor); + } else { + item = new LocalVideo(path, app, cursor); + } + } else { + item.updateContent(cursor); + } + return item; + } + + // The pids array are sorted by the (path) id. + public static MediaItem[] getMediaItemById( + GalleryApp application, boolean isImage, ArrayList ids) { + // get the lower and upper bound of (path) id + MediaItem[] result = new MediaItem[ids.size()]; + if (ids.isEmpty()) return result; + int idLow = ids.get(0); + int idHigh = ids.get(ids.size() - 1); + + // prepare the query parameters + Uri baseUri; + String[] projection; + Path itemPath; + if (isImage) { + baseUri = Images.Media.EXTERNAL_CONTENT_URI; + projection = LocalImage.PROJECTION; + itemPath = LocalImage.ITEM_PATH; + } else { + baseUri = Video.Media.EXTERNAL_CONTENT_URI; + projection = LocalVideo.PROJECTION; + itemPath = LocalVideo.ITEM_PATH; + } + + ContentResolver resolver = application.getContentResolver(); + DataManager dataManager = application.getDataManager(); + Cursor cursor = resolver.query(baseUri, projection, "_id BETWEEN ? AND ?", + new String[]{String.valueOf(idLow), String.valueOf(idHigh)}, + "_id"); + if (cursor == null) { + Log.w(TAG, "query fail" + baseUri); + return result; + } + try { + int n = ids.size(); + int i = 0; + + while (i < n && cursor.moveToNext()) { + int id = cursor.getInt(0); // _id must be in the first column + + // Match id with the one on the ids list. + if (ids.get(i) > id) { + continue; + } + + while (ids.get(i) < id) { + if (++i >= n) { + return result; + } + } + + Path childPath = itemPath.getChild(id); + MediaItem item = loadOrUpdateItem(childPath, cursor, dataManager, + application, isImage); + result[i] = item; + ++i; + } + return result; + } finally { + cursor.close(); + } + } + + public static Cursor getItemCursor(ContentResolver resolver, Uri uri, + String[] projection, int id) { + return resolver.query(uri, projection, "_id=?", + new String[]{String.valueOf(id)}, null); + } + + @Override + public int getMediaItemCount() { + if (mCachedCount == INVALID_COUNT) { + Cursor cursor = mResolver.query( + mBaseUri, COUNT_PROJECTION, mWhereClause, + new String[]{String.valueOf(mBucketId)}, null); + if (cursor == null) { + Log.w(TAG, "query fail"); + return 0; + } + try { + Utils.assertTrue(cursor.moveToNext()); + mCachedCount = cursor.getInt(0); + } finally { + cursor.close(); + } + } + return mCachedCount; + } + + @Override + public String getName() { + return mBucketName; + } + + @Override + public long reload() { + if (mNotifier.isDirty()) { + mDataVersion = nextVersionNumber(); + mCachedCount = INVALID_COUNT; + } + return mDataVersion; + } + + @Override + public int getSupportedOperations() { + return SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_INFO; + } + + @Override + public void delete() { + GalleryUtils.assertNotInRenderThread(); + mResolver.delete(mBaseUri, mWhereClause, + new String[]{String.valueOf(mBucketId)}); + } + + @Override + public boolean isLeafAlbum() { + return true; + } +} diff --git a/src/com/android/gallery3d/data/LocalAlbumSet.java b/src/com/android/gallery3d/data/LocalAlbumSet.java new file mode 100644 index 000000000..60bef9a33 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalAlbumSet.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.MediaSetUtils; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore.Files; +import android.provider.MediaStore.Files.FileColumns; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.provider.MediaStore.Video; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; + +// LocalAlbumSet lists all image or video albums in the local storage. +// The path should be "/local/image", "local/video" or "/local/all" +public class LocalAlbumSet extends MediaSet { + public static final Path PATH_ALL = Path.fromString("/local/all"); + public static final Path PATH_IMAGE = Path.fromString("/local/image"); + public static final Path PATH_VIDEO = Path.fromString("/local/video"); + + private static final String TAG = "LocalAlbumSet"; + private static final String EXTERNAL_MEDIA = "external"; + + // The indices should match the following projections. + private static final int INDEX_BUCKET_ID = 0; + private static final int INDEX_MEDIA_TYPE = 1; + private static final int INDEX_BUCKET_NAME = 2; + + private static final Uri mBaseUri = Files.getContentUri(EXTERNAL_MEDIA); + private static final Uri mWatchUriImage = Images.Media.EXTERNAL_CONTENT_URI; + private static final Uri mWatchUriVideo = Video.Media.EXTERNAL_CONTENT_URI; + + // The order is import it must match to the index in MediaStore. + private static final String[] PROJECTION_BUCKET = { + ImageColumns.BUCKET_ID, + FileColumns.MEDIA_TYPE, + ImageColumns.BUCKET_DISPLAY_NAME }; + + private final GalleryApp mApplication; + private final int mType; + private ArrayList mAlbums = new ArrayList(); + private final ChangeNotifier mNotifierImage; + private final ChangeNotifier mNotifierVideo; + private final String mName; + + public LocalAlbumSet(Path path, GalleryApp application) { + super(path, nextVersionNumber()); + mApplication = application; + mType = getTypeFromPath(path); + mNotifierImage = new ChangeNotifier(this, mWatchUriImage, application); + mNotifierVideo = new ChangeNotifier(this, mWatchUriVideo, application); + mName = application.getResources().getString( + R.string.set_label_local_albums); + } + + private static int getTypeFromPath(Path path) { + String name[] = path.split(); + if (name.length < 2) { + throw new IllegalArgumentException(path.toString()); + } + if ("all".equals(name[1])) return MEDIA_TYPE_ALL; + if ("image".equals(name[1])) return MEDIA_TYPE_IMAGE; + if ("video".equals(name[1])) return MEDIA_TYPE_VIDEO; + throw new IllegalArgumentException(path.toString()); + } + + @Override + public MediaSet getSubMediaSet(int index) { + return mAlbums.get(index); + } + + @Override + public int getSubMediaSetCount() { + return mAlbums.size(); + } + + @Override + public String getName() { + return mName; + } + + private BucketEntry[] loadBucketEntries(Cursor cursor) { + HashSet buffer = new HashSet(); + int typeBits = 0; + if ((mType & MEDIA_TYPE_IMAGE) != 0) { + typeBits |= (1 << FileColumns.MEDIA_TYPE_IMAGE); + } + if ((mType & MEDIA_TYPE_VIDEO) != 0) { + typeBits |= (1 << FileColumns.MEDIA_TYPE_VIDEO); + } + try { + while (cursor.moveToNext()) { + if ((typeBits & (1 << cursor.getInt(INDEX_MEDIA_TYPE))) != 0) { + buffer.add(new BucketEntry( + cursor.getInt(INDEX_BUCKET_ID), + cursor.getString(INDEX_BUCKET_NAME))); + } + } + } finally { + cursor.close(); + } + return buffer.toArray(new BucketEntry[buffer.size()]); + } + + + private static int findBucket(BucketEntry entries[], int bucketId) { + for (int i = 0, n = entries.length; i < n ; ++i) { + if (entries[i].bucketId == bucketId) return i; + } + return -1; + } + + @SuppressWarnings("unchecked") + protected ArrayList loadSubMediaSets() { + // Note: it will be faster if we only select media_type and bucket_id. + // need to test the performance if that is worth + + Uri uri = mBaseUri.buildUpon(). + appendQueryParameter("distinct", "true").build(); + GalleryUtils.assertNotInRenderThread(); + Cursor cursor = mApplication.getContentResolver().query( + uri, PROJECTION_BUCKET, null, null, null); + if (cursor == null) { + Log.w(TAG, "cannot open local database: " + uri); + return new ArrayList(); + } + BucketEntry[] entries = loadBucketEntries(cursor); + int offset = 0; + + int index = findBucket(entries, MediaSetUtils.CAMERA_BUCKET_ID); + if (index != -1) { + Utils.swap(entries, index, offset++); + } + index = findBucket(entries, MediaSetUtils.DOWNLOAD_BUCKET_ID); + if (index != -1) { + Utils.swap(entries, index, offset++); + } + + Arrays.sort(entries, offset, entries.length, new Comparator() { + @Override + public int compare(BucketEntry a, BucketEntry b) { + int result = a.bucketName.compareTo(b.bucketName); + return result != 0 + ? result + : Utils.compare(a.bucketId, b.bucketId); + } + }); + ArrayList albums = new ArrayList(); + DataManager dataManager = mApplication.getDataManager(); + for (BucketEntry entry : entries) { + albums.add(getLocalAlbum(dataManager, + mType, mPath, entry.bucketId, entry.bucketName)); + } + for (int i = 0, n = albums.size(); i < n; ++i) { + albums.get(i).reload(); + } + return albums; + } + + private MediaSet getLocalAlbum( + DataManager manager, int type, Path parent, int id, String name) { + Path path = parent.getChild(id); + MediaObject object = manager.peekMediaObject(path); + if (object != null) return (MediaSet) object; + switch (type) { + case MEDIA_TYPE_IMAGE: + return new LocalAlbum(path, mApplication, id, true, name); + case MEDIA_TYPE_VIDEO: + return new LocalAlbum(path, mApplication, id, false, name); + case MEDIA_TYPE_ALL: + Comparator comp = DataManager.sDateTakenComparator; + return new LocalMergeAlbum(path, comp, new MediaSet[] { + getLocalAlbum(manager, MEDIA_TYPE_IMAGE, PATH_IMAGE, id, name), + getLocalAlbum(manager, MEDIA_TYPE_VIDEO, PATH_VIDEO, id, name)}); + } + throw new IllegalArgumentException(String.valueOf(type)); + } + + public static String getBucketName(ContentResolver resolver, int bucketId) { + Uri uri = mBaseUri.buildUpon() + .appendQueryParameter("limit", "1") + .build(); + + Cursor cursor = resolver.query( + uri, PROJECTION_BUCKET, "bucket_id = ?", + new String[]{String.valueOf(bucketId)}, null); + + if (cursor == null) { + Log.w(TAG, "query fail: " + uri); + return ""; + } + try { + return cursor.moveToNext() + ? cursor.getString(INDEX_BUCKET_NAME) + : ""; + } finally { + cursor.close(); + } + } + + @Override + public long reload() { + // "|" is used instead of "||" because we want to clear both flags. + if (mNotifierImage.isDirty() | mNotifierVideo.isDirty()) { + mDataVersion = nextVersionNumber(); + mAlbums = loadSubMediaSets(); + } + return mDataVersion; + } + + // For debug only. Fake there is a ContentObserver.onChange() event. + void fakeChange() { + mNotifierImage.fakeChange(); + mNotifierVideo.fakeChange(); + } + + private static class BucketEntry { + public String bucketName; + public int bucketId; + + public BucketEntry(int id, String name) { + bucketId = id; + bucketName = Utils.ensureNotNull(name); + } + + @Override + public int hashCode() { + return bucketId; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof BucketEntry)) return false; + BucketEntry entry = (BucketEntry) object; + return bucketId == entry.bucketId; + } + } +} diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java new file mode 100644 index 000000000..f3dedf037 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalImage.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.util.UpdateHelper; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.media.ExifInterface; +import android.net.Uri; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; + +import java.io.File; +import java.io.IOException; + +// LocalImage represents an image in the local storage. +public class LocalImage extends LocalMediaItem { + private static final int THUMBNAIL_TARGET_SIZE = 640; + private static final int MICROTHUMBNAIL_TARGET_SIZE = 200; + + private static final String TAG = "LocalImage"; + + static final Path ITEM_PATH = Path.fromString("/local/image/item"); + + // Must preserve order between these indices and the order of the terms in + // the following PROJECTION array. + private static final int INDEX_ID = 0; + private static final int INDEX_CAPTION = 1; + private static final int INDEX_MIME_TYPE = 2; + private static final int INDEX_LATITUDE = 3; + private static final int INDEX_LONGITUDE = 4; + private static final int INDEX_DATE_TAKEN = 5; + private static final int INDEX_DATE_ADDED = 6; + private static final int INDEX_DATE_MODIFIED = 7; + private static final int INDEX_DATA = 8; + private static final int INDEX_ORIENTATION = 9; + private static final int INDEX_BUCKET_ID = 10; + private static final int INDEX_SIZE_ID = 11; + + static final String[] PROJECTION = { + ImageColumns._ID, // 0 + ImageColumns.TITLE, // 1 + ImageColumns.MIME_TYPE, // 2 + ImageColumns.LATITUDE, // 3 + ImageColumns.LONGITUDE, // 4 + ImageColumns.DATE_TAKEN, // 5 + ImageColumns.DATE_ADDED, // 6 + ImageColumns.DATE_MODIFIED, // 7 + ImageColumns.DATA, // 8 + ImageColumns.ORIENTATION, // 9 + ImageColumns.BUCKET_ID, // 10 + ImageColumns.SIZE // 11 + }; + + private final GalleryApp mApplication; + + public int rotation; + + public LocalImage(Path path, GalleryApp application, Cursor cursor) { + super(path, nextVersionNumber()); + mApplication = application; + loadFromCursor(cursor); + } + + public LocalImage(Path path, GalleryApp application, int id) { + super(path, nextVersionNumber()); + mApplication = application; + ContentResolver resolver = mApplication.getContentResolver(); + Uri uri = Images.Media.EXTERNAL_CONTENT_URI; + Cursor cursor = LocalAlbum.getItemCursor(resolver, uri, PROJECTION, id); + if (cursor == null) { + throw new RuntimeException("cannot get cursor for: " + path); + } + try { + if (cursor.moveToNext()) { + loadFromCursor(cursor); + } else { + throw new RuntimeException("cannot find data for: " + path); + } + } finally { + cursor.close(); + } + } + + private void loadFromCursor(Cursor cursor) { + id = cursor.getInt(INDEX_ID); + caption = cursor.getString(INDEX_CAPTION); + mimeType = cursor.getString(INDEX_MIME_TYPE); + latitude = cursor.getDouble(INDEX_LATITUDE); + longitude = cursor.getDouble(INDEX_LONGITUDE); + dateTakenInMs = cursor.getLong(INDEX_DATE_TAKEN); + filePath = cursor.getString(INDEX_DATA); + rotation = cursor.getInt(INDEX_ORIENTATION); + bucketId = cursor.getInt(INDEX_BUCKET_ID); + fileSize = cursor.getLong(INDEX_SIZE_ID); + } + + @Override + protected boolean updateFromCursor(Cursor cursor) { + UpdateHelper uh = new UpdateHelper(); + id = uh.update(id, cursor.getInt(INDEX_ID)); + caption = uh.update(caption, cursor.getString(INDEX_CAPTION)); + mimeType = uh.update(mimeType, cursor.getString(INDEX_MIME_TYPE)); + latitude = uh.update(latitude, cursor.getDouble(INDEX_LATITUDE)); + longitude = uh.update(longitude, cursor.getDouble(INDEX_LONGITUDE)); + dateTakenInMs = uh.update( + dateTakenInMs, cursor.getLong(INDEX_DATE_TAKEN)); + dateAddedInSec = uh.update( + dateAddedInSec, cursor.getLong(INDEX_DATE_ADDED)); + dateModifiedInSec = uh.update( + dateModifiedInSec, cursor.getLong(INDEX_DATE_MODIFIED)); + filePath = uh.update(filePath, cursor.getString(INDEX_DATA)); + rotation = uh.update(rotation, cursor.getInt(INDEX_ORIENTATION)); + bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID)); + fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE_ID)); + return uh.isUpdated(); + } + + @Override + public Job requestImage(int type) { + return new LocalImageRequest(mApplication, mPath, type, filePath); + } + + public static class LocalImageRequest extends ImageCacheRequest { + private String mLocalFilePath; + + LocalImageRequest(GalleryApp application, Path path, int type, + String localFilePath) { + super(application, path, type, getTargetSize(type)); + mLocalFilePath = localFilePath; + } + + @Override + public Bitmap onDecodeOriginal(JobContext jc, int type) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + return DecodeUtils.requestDecode( + jc, mLocalFilePath, options, getTargetSize(type)); + } + } + + static int getTargetSize(int type) { + switch (type) { + case TYPE_THUMBNAIL: + return THUMBNAIL_TARGET_SIZE; + case TYPE_MICROTHUMBNAIL: + return MICROTHUMBNAIL_TARGET_SIZE; + default: + throw new RuntimeException( + "should only request thumb/microthumb from cache"); + } + } + + @Override + public Job requestLargeImage() { + return new LocalLargeImageRequest(filePath); + } + + public static class LocalLargeImageRequest + implements Job { + String mLocalFilePath; + + public LocalLargeImageRequest(String localFilePath) { + mLocalFilePath = localFilePath; + } + + public BitmapRegionDecoder run(JobContext jc) { + return DecodeUtils.requestCreateBitmapRegionDecoder( + jc, mLocalFilePath, false); + } + } + + @Override + public int getSupportedOperations() { + int operation = SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_CROP + | SUPPORT_SETAS | SUPPORT_EDIT | SUPPORT_INFO; + if (BitmapUtils.isSupportedByRegionDecoder(mimeType)) { + operation |= SUPPORT_FULL_IMAGE; + } + + if (BitmapUtils.isRotationSupported(mimeType)) { + operation |= SUPPORT_ROTATE; + } + + if (GalleryUtils.isValidLocation(latitude, longitude)) { + operation |= SUPPORT_SHOW_ON_MAP; + } + return operation; + } + + @Override + public void delete() { + GalleryUtils.assertNotInRenderThread(); + Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI; + mApplication.getContentResolver().delete(baseUri, "_id=?", + new String[]{String.valueOf(id)}); + } + + private static String getExifOrientation(int orientation) { + switch (orientation) { + case 0: + return String.valueOf(ExifInterface.ORIENTATION_NORMAL); + case 90: + return String.valueOf(ExifInterface.ORIENTATION_ROTATE_90); + case 180: + return String.valueOf(ExifInterface.ORIENTATION_ROTATE_180); + case 270: + return String.valueOf(ExifInterface.ORIENTATION_ROTATE_270); + default: + throw new AssertionError("invalid: " + orientation); + } + } + + @Override + public void rotate(int degrees) { + GalleryUtils.assertNotInRenderThread(); + Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI; + ContentValues values = new ContentValues(); + int rotation = (this.rotation + degrees) % 360; + if (rotation < 0) rotation += 360; + + if (mimeType.equalsIgnoreCase("image/jpeg")) { + try { + ExifInterface exif = new ExifInterface(filePath); + exif.setAttribute(ExifInterface.TAG_ORIENTATION, + getExifOrientation(rotation)); + exif.saveAttributes(); + } catch (IOException e) { + Log.w(TAG, "cannot set exif data: " + filePath); + } + + // We need to update the filesize as well + fileSize = new File(filePath).length(); + values.put(Images.Media.SIZE, fileSize); + } + + values.put(Images.Media.ORIENTATION, rotation); + mApplication.getContentResolver().update(baseUri, values, "_id=?", + new String[]{String.valueOf(id)}); + } + + @Override + public Uri getContentUri() { + Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI; + return baseUri.buildUpon().appendPath(String.valueOf(id)).build(); + } + + @Override + public int getMediaType() { + return MEDIA_TYPE_IMAGE; + } + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + details.addDetail(MediaDetails.INDEX_ORIENTATION, Integer.valueOf(rotation)); + MediaDetails.extractExifInfo(details, filePath); + return details; + } + + @Override + public int getRotation() { + return rotation; + } +} diff --git a/src/com/android/gallery3d/data/LocalMediaItem.java b/src/com/android/gallery3d/data/LocalMediaItem.java new file mode 100644 index 000000000..a76fedf32 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalMediaItem.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.util.GalleryUtils; + +import android.database.Cursor; + +import java.text.DateFormat; +import java.util.Date; + +// +// LocalMediaItem is an abstract class captures those common fields +// in LocalImage and LocalVideo. +// +public abstract class LocalMediaItem extends MediaItem { + + @SuppressWarnings("unused") + private static final String TAG = "LocalMediaItem"; + + // database fields + public int id; + public String caption; + public String mimeType; + public long fileSize; + public double latitude = INVALID_LATLNG; + public double longitude = INVALID_LATLNG; + public long dateTakenInMs; + public long dateAddedInSec; + public long dateModifiedInSec; + public String filePath; + public int bucketId; + + public LocalMediaItem(Path path, long version) { + super(path, version); + } + + @Override + public long getDateInMs() { + return dateTakenInMs; + } + + @Override + public String getName() { + return caption; + } + + @Override + public void getLatLong(double[] latLong) { + latLong[0] = latitude; + latLong[1] = longitude; + } + + abstract protected boolean updateFromCursor(Cursor cursor); + + public int getBucketId() { + return bucketId; + } + + protected void updateContent(Cursor cursor) { + if (updateFromCursor(cursor)) { + mDataVersion = nextVersionNumber(); + } + } + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + details.addDetail(MediaDetails.INDEX_PATH, filePath); + details.addDetail(MediaDetails.INDEX_TITLE, caption); + DateFormat formater = DateFormat.getDateTimeInstance(); + details.addDetail(MediaDetails.INDEX_DATETIME, formater.format(new Date(dateTakenInMs))); + + if (GalleryUtils.isValidLocation(latitude, longitude)) { + details.addDetail(MediaDetails.INDEX_LOCATION, new double[] {latitude, longitude}); + } + if (fileSize > 0) details.addDetail(MediaDetails.INDEX_SIZE, fileSize); + return details; + } + + @Override + public String getMimeType() { + return mimeType; + } + + public long getSize() { + return fileSize; + } +} diff --git a/src/com/android/gallery3d/data/LocalMergeAlbum.java b/src/com/android/gallery3d/data/LocalMergeAlbum.java new file mode 100644 index 000000000..bb796d53a --- /dev/null +++ b/src/com/android/gallery3d/data/LocalMergeAlbum.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.SortedMap; +import java.util.TreeMap; + +// MergeAlbum merges items from two or more MediaSets. It uses a Comparator to +// determine the order of items. The items are assumed to be sorted in the input +// media sets (with the same order that the Comparator uses). +// +// This only handles MediaItems, not SubMediaSets. +public class LocalMergeAlbum extends MediaSet implements ContentListener { + @SuppressWarnings("unused") + private static final String TAG = "LocalMergeAlbum"; + private static final int PAGE_SIZE = 64; + + private final Comparator mComparator; + private final MediaSet[] mSources; + + private String mName; + private FetchCache[] mFetcher; + private int mSupportedOperation; + + // mIndex maps global position to the position of each underlying media sets. + private TreeMap mIndex = new TreeMap(); + + public LocalMergeAlbum( + Path path, Comparator comparator, MediaSet[] sources) { + super(path, INVALID_DATA_VERSION); + mComparator = comparator; + mSources = sources; + mName = sources.length == 0 ? "" : sources[0].getName(); + for (MediaSet set : mSources) { + set.addContentListener(this); + } + } + + private void updateData() { + ArrayList matches = new ArrayList(); + int supported = mSources.length == 0 ? 0 : MediaItem.SUPPORT_ALL; + mFetcher = new FetchCache[mSources.length]; + for (int i = 0, n = mSources.length; i < n; ++i) { + mFetcher[i] = new FetchCache(mSources[i]); + supported &= mSources[i].getSupportedOperations(); + } + mSupportedOperation = supported; + mIndex.clear(); + mIndex.put(0, new int[mSources.length]); + mName = mSources.length == 0 ? "" : mSources[0].getName(); + } + + private void invalidateCache() { + for (int i = 0, n = mSources.length; i < n; i++) { + mFetcher[i].invalidate(); + } + mIndex.clear(); + mIndex.put(0, new int[mSources.length]); + } + + @Override + public String getName() { + return mName; + } + + @Override + public int getMediaItemCount() { + return getTotalMediaItemCount(); + } + + @Override + public ArrayList getMediaItem(int start, int count) { + + // First find the nearest mark position <= start. + SortedMap head = mIndex.headMap(start + 1); + int markPos = head.lastKey(); + int[] subPos = head.get(markPos).clone(); + MediaItem[] slot = new MediaItem[mSources.length]; + + int size = mSources.length; + + // fill all slots + for (int i = 0; i < size; i++) { + slot[i] = mFetcher[i].getItem(subPos[i]); + } + + ArrayList result = new ArrayList(); + + for (int i = markPos; i < start + count; i++) { + int k = -1; // k points to the best slot up to now. + for (int j = 0; j < size; j++) { + if (slot[j] != null) { + if (k == -1 || mComparator.compare(slot[j], slot[k]) < 0) { + k = j; + } + } + } + + // If we don't have anything, all streams are exhausted. + if (k == -1) break; + + // Pick the best slot and refill it. + subPos[k]++; + if (i >= start) { + result.add(slot[k]); + } + slot[k] = mFetcher[k].getItem(subPos[k]); + + // Periodically leave a mark in the index, so we can come back later. + if ((i + 1) % PAGE_SIZE == 0) { + mIndex.put(i + 1, subPos.clone()); + } + } + + return result; + } + + @Override + public int getTotalMediaItemCount() { + int count = 0; + for (MediaSet set : mSources) { + count += set.getTotalMediaItemCount(); + } + return count; + } + + @Override + public long reload() { + boolean changed = false; + for (int i = 0, n = mSources.length; i < n; ++i) { + if (mSources[i].reload() > mDataVersion) changed = true; + } + if (changed) { + mDataVersion = nextVersionNumber(); + updateData(); + invalidateCache(); + } + return mDataVersion; + } + + @Override + public void onContentDirty() { + notifyContentChanged(); + } + + @Override + public int getSupportedOperations() { + return mSupportedOperation; + } + + @Override + public void delete() { + for (MediaSet set : mSources) { + set.delete(); + } + } + + @Override + public void rotate(int degrees) { + for (MediaSet set : mSources) { + set.rotate(degrees); + } + } + + private static class FetchCache { + private MediaSet mBaseSet; + private SoftReference> mCacheRef; + private int mStartPos; + + public FetchCache(MediaSet baseSet) { + mBaseSet = baseSet; + } + + public void invalidate() { + mCacheRef = null; + } + + public MediaItem getItem(int index) { + boolean needLoading = false; + ArrayList cache = null; + if (mCacheRef == null + || index < mStartPos || index >= mStartPos + PAGE_SIZE) { + needLoading = true; + } else { + cache = mCacheRef.get(); + if (cache == null) { + needLoading = true; + } + } + + if (needLoading) { + cache = mBaseSet.getMediaItem(index, PAGE_SIZE); + mCacheRef = new SoftReference>(cache); + mStartPos = index; + } + + if (index < mStartPos || index >= mStartPos + cache.size()) { + return null; + } + + return cache.get(index - mStartPos); + } + } + + @Override + public boolean isLeafAlbum() { + return true; + } +} diff --git a/src/com/android/gallery3d/data/LocalSource.java b/src/com/android/gallery3d/data/LocalSource.java new file mode 100644 index 000000000..58ac22490 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalSource.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.Gallery; +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.data.MediaSet.ItemConsumer; + +import android.content.ContentProviderClient; +import android.content.ContentUris; +import android.content.UriMatcher; +import android.net.Uri; +import android.provider.MediaStore; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +class LocalSource extends MediaSource { + + public static final String KEY_BUCKET_ID = "bucketId"; + + private GalleryApp mApplication; + private PathMatcher mMatcher; + private static final int NO_MATCH = -1; + private final UriMatcher mUriMatcher = new UriMatcher(NO_MATCH); + public static final Comparator sIdComparator = new IdComparator(); + + private static final int LOCAL_IMAGE_ALBUMSET = 0; + private static final int LOCAL_VIDEO_ALBUMSET = 1; + private static final int LOCAL_IMAGE_ALBUM = 2; + private static final int LOCAL_VIDEO_ALBUM = 3; + private static final int LOCAL_IMAGE_ITEM = 4; + private static final int LOCAL_VIDEO_ITEM = 5; + private static final int LOCAL_ALL_ALBUMSET = 6; + private static final int LOCAL_ALL_ALBUM = 7; + + private static final String TAG = "LocalSource"; + + private ContentProviderClient mClient; + + public LocalSource(GalleryApp context) { + super("local"); + mApplication = context; + mMatcher = new PathMatcher(); + mMatcher.add("/local/image", LOCAL_IMAGE_ALBUMSET); + mMatcher.add("/local/video", LOCAL_VIDEO_ALBUMSET); + mMatcher.add("/local/all", LOCAL_ALL_ALBUMSET); + + mMatcher.add("/local/image/*", LOCAL_IMAGE_ALBUM); + mMatcher.add("/local/video/*", LOCAL_VIDEO_ALBUM); + mMatcher.add("/local/all/*", LOCAL_ALL_ALBUM); + mMatcher.add("/local/image/item/*", LOCAL_IMAGE_ITEM); + mMatcher.add("/local/video/item/*", LOCAL_VIDEO_ITEM); + + mUriMatcher.addURI(MediaStore.AUTHORITY, + "external/images/media/#", LOCAL_IMAGE_ITEM); + mUriMatcher.addURI(MediaStore.AUTHORITY, + "external/video/media/#", LOCAL_VIDEO_ITEM); + mUriMatcher.addURI(MediaStore.AUTHORITY, + "external/images/media", LOCAL_IMAGE_ALBUM); + mUriMatcher.addURI(MediaStore.AUTHORITY, + "external/video/media", LOCAL_VIDEO_ALBUM); + } + + @Override + public MediaObject createMediaObject(Path path) { + GalleryApp app = mApplication; + switch (mMatcher.match(path)) { + case LOCAL_ALL_ALBUMSET: + case LOCAL_IMAGE_ALBUMSET: + case LOCAL_VIDEO_ALBUMSET: + return new LocalAlbumSet(path, mApplication); + case LOCAL_IMAGE_ALBUM: + return new LocalAlbum(path, app, mMatcher.getIntVar(0), true); + case LOCAL_VIDEO_ALBUM: + return new LocalAlbum(path, app, mMatcher.getIntVar(0), false); + case LOCAL_ALL_ALBUM: { + int bucketId = mMatcher.getIntVar(0); + DataManager dataManager = app.getDataManager(); + MediaSet imageSet = (MediaSet) dataManager.getMediaObject( + LocalAlbumSet.PATH_IMAGE.getChild(bucketId)); + MediaSet videoSet = (MediaSet) dataManager.getMediaObject( + LocalAlbumSet.PATH_VIDEO.getChild(bucketId)); + Comparator comp = DataManager.sDateTakenComparator; + return new LocalMergeAlbum( + path, comp, new MediaSet[] {imageSet, videoSet}); + } + case LOCAL_IMAGE_ITEM: + return new LocalImage(path, mApplication, mMatcher.getIntVar(0)); + case LOCAL_VIDEO_ITEM: + return new LocalVideo(path, mApplication, mMatcher.getIntVar(0)); + default: + throw new RuntimeException("bad path: " + path); + } + } + + private static int getMediaType(String type, int defaultType) { + if (type == null) return defaultType; + try { + int value = Integer.parseInt(type); + if ((value & (MEDIA_TYPE_IMAGE + | MEDIA_TYPE_VIDEO)) != 0) return value; + } catch (NumberFormatException e) { + Log.w(TAG, "invalid type: " + type, e); + } + return defaultType; + } + + // The media type bit passed by the intent + private static final int MEDIA_TYPE_IMAGE = 1; + private static final int MEDIA_TYPE_VIDEO = 4; + + private Path getAlbumPath(Uri uri, int defaultType) { + int mediaType = getMediaType( + uri.getQueryParameter(Gallery.KEY_MEDIA_TYPES), + defaultType); + String bucketId = uri.getQueryParameter(KEY_BUCKET_ID); + int id = 0; + try { + id = Integer.parseInt(bucketId); + } catch (NumberFormatException e) { + Log.w(TAG, "invalid bucket id: " + bucketId, e); + return null; + } + switch (mediaType) { + case MEDIA_TYPE_IMAGE: + return Path.fromString("/local/image").getChild(id); + case MEDIA_TYPE_VIDEO: + return Path.fromString("/local/video").getChild(id); + default: + return Path.fromString("/merge/{/local/image,/local/video}") + .getChild(id); + } + } + + @Override + public Path findPathByUri(Uri uri) { + try { + switch (mUriMatcher.match(uri)) { + case LOCAL_IMAGE_ITEM: { + long id = ContentUris.parseId(uri); + return id >= 0 ? LocalImage.ITEM_PATH.getChild(id) : null; + } + case LOCAL_VIDEO_ITEM: { + long id = ContentUris.parseId(uri); + return id >= 0 ? LocalVideo.ITEM_PATH.getChild(id) : null; + } + case LOCAL_IMAGE_ALBUM: { + return getAlbumPath(uri, MEDIA_TYPE_IMAGE); + } + case LOCAL_VIDEO_ALBUM: { + return getAlbumPath(uri, MEDIA_TYPE_VIDEO); + } + } + } catch (NumberFormatException e) { + Log.w(TAG, "uri: " + uri.toString(), e); + } + return null; + } + + @Override + public Path getDefaultSetOf(Path item) { + MediaObject object = mApplication.getDataManager().getMediaObject(item); + if (object instanceof LocalImage) { + return Path.fromString("/local/image/").getChild( + String.valueOf(((LocalImage) object).getBucketId())); + } else if (object instanceof LocalVideo) { + return Path.fromString("/local/video/").getChild( + String.valueOf(((LocalVideo) object).getBucketId())); + } + return null; + } + + @Override + public void mapMediaItems(ArrayList list, ItemConsumer consumer) { + ArrayList imageList = new ArrayList(); + ArrayList videoList = new ArrayList(); + int n = list.size(); + for (int i = 0; i < n; i++) { + PathId pid = list.get(i); + // We assume the form is: "/local/{image,video}/item/#" + // We don't use mMatcher for efficiency's reason. + Path parent = pid.path.getParent(); + if (parent == LocalImage.ITEM_PATH) { + imageList.add(pid); + } else if (parent == LocalVideo.ITEM_PATH) { + videoList.add(pid); + } + } + // TODO: use "files" table so we can merge the two cases. + processMapMediaItems(imageList, consumer, true); + processMapMediaItems(videoList, consumer, false); + } + + private void processMapMediaItems(ArrayList list, + ItemConsumer consumer, boolean isImage) { + // Sort path by path id + Collections.sort(list, sIdComparator); + int n = list.size(); + for (int i = 0; i < n; ) { + PathId pid = list.get(i); + + // Find a range of items. + ArrayList ids = new ArrayList(); + int startId = Integer.parseInt(pid.path.getSuffix()); + ids.add(startId); + + int j; + for (j = i + 1; j < n; j++) { + PathId pid2 = list.get(j); + int curId = Integer.parseInt(pid2.path.getSuffix()); + if (curId - startId >= MediaSet.MEDIAITEM_BATCH_FETCH_COUNT) { + break; + } + ids.add(curId); + } + + MediaItem[] items = LocalAlbum.getMediaItemById( + mApplication, isImage, ids); + for(int k = i ; k < j; k++) { + PathId pid2 = list.get(k); + consumer.consume(pid2.id, items[k - i]); + } + + i = j; + } + } + + // This is a comparator which compares the suffix number in two Paths. + private static class IdComparator implements Comparator { + public int compare(PathId p1, PathId p2) { + String s1 = p1.path.getSuffix(); + String s2 = p2.path.getSuffix(); + int len1 = s1.length(); + int len2 = s2.length(); + if (len1 < len2) { + return -1; + } else if (len1 > len2) { + return 1; + } else { + return s1.compareTo(s2); + } + } + } + + @Override + public void resume() { + mClient = mApplication.getContentResolver() + .acquireContentProviderClient(MediaStore.AUTHORITY); + } + + @Override + public void pause() { + mClient.release(); + mClient = null; + } +} diff --git a/src/com/android/gallery3d/data/LocalVideo.java b/src/com/android/gallery3d/data/LocalVideo.java new file mode 100644 index 000000000..d1498e856 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalVideo.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.util.UpdateHelper; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.net.Uri; +import android.provider.MediaStore.Video; +import android.provider.MediaStore.Video.VideoColumns; + +import java.io.File; + +// LocalVideo represents a video in the local storage. +public class LocalVideo extends LocalMediaItem { + + static final Path ITEM_PATH = Path.fromString("/local/video/item"); + + // Must preserve order between these indices and the order of the terms in + // the following PROJECTION array. + private static final int INDEX_ID = 0; + private static final int INDEX_CAPTION = 1; + private static final int INDEX_MIME_TYPE = 2; + private static final int INDEX_LATITUDE = 3; + private static final int INDEX_LONGITUDE = 4; + private static final int INDEX_DATE_TAKEN = 5; + private static final int INDEX_DATE_ADDED = 6; + private static final int INDEX_DATE_MODIFIED = 7; + private static final int INDEX_DATA = 8; + private static final int INDEX_DURATION = 9; + private static final int INDEX_BUCKET_ID = 10; + private static final int INDEX_SIZE_ID = 11; + + static final String[] PROJECTION = new String[] { + VideoColumns._ID, + VideoColumns.TITLE, + VideoColumns.MIME_TYPE, + VideoColumns.LATITUDE, + VideoColumns.LONGITUDE, + VideoColumns.DATE_TAKEN, + VideoColumns.DATE_ADDED, + VideoColumns.DATE_MODIFIED, + VideoColumns.DATA, + VideoColumns.DURATION, + VideoColumns.BUCKET_ID, + VideoColumns.SIZE + }; + + private final GalleryApp mApplication; + private static Bitmap sOverlay; + + public int durationInSec; + + public LocalVideo(Path path, GalleryApp application, Cursor cursor) { + super(path, nextVersionNumber()); + mApplication = application; + loadFromCursor(cursor); + } + + public LocalVideo(Path path, GalleryApp context, int id) { + super(path, nextVersionNumber()); + mApplication = context; + ContentResolver resolver = mApplication.getContentResolver(); + Uri uri = Video.Media.EXTERNAL_CONTENT_URI; + Cursor cursor = LocalAlbum.getItemCursor(resolver, uri, PROJECTION, id); + if (cursor == null) { + throw new RuntimeException("cannot get cursor for: " + path); + } + try { + if (cursor.moveToNext()) { + loadFromCursor(cursor); + } else { + throw new RuntimeException("cannot find data for: " + path); + } + } finally { + cursor.close(); + } + } + + private void loadFromCursor(Cursor cursor) { + id = cursor.getInt(INDEX_ID); + caption = cursor.getString(INDEX_CAPTION); + mimeType = cursor.getString(INDEX_MIME_TYPE); + latitude = cursor.getDouble(INDEX_LATITUDE); + longitude = cursor.getDouble(INDEX_LONGITUDE); + dateTakenInMs = cursor.getLong(INDEX_DATE_TAKEN); + filePath = cursor.getString(INDEX_DATA); + durationInSec = cursor.getInt(INDEX_DURATION) / 1000; + bucketId = cursor.getInt(INDEX_BUCKET_ID); + fileSize = cursor.getLong(INDEX_SIZE_ID); + } + + @Override + protected boolean updateFromCursor(Cursor cursor) { + UpdateHelper uh = new UpdateHelper(); + id = uh.update(id, cursor.getInt(INDEX_ID)); + caption = uh.update(caption, cursor.getString(INDEX_CAPTION)); + mimeType = uh.update(mimeType, cursor.getString(INDEX_MIME_TYPE)); + latitude = uh.update(latitude, cursor.getDouble(INDEX_LATITUDE)); + longitude = uh.update(longitude, cursor.getDouble(INDEX_LONGITUDE)); + dateTakenInMs = uh.update( + dateTakenInMs, cursor.getLong(INDEX_DATE_TAKEN)); + dateAddedInSec = uh.update( + dateAddedInSec, cursor.getLong(INDEX_DATE_ADDED)); + dateModifiedInSec = uh.update( + dateModifiedInSec, cursor.getLong(INDEX_DATE_MODIFIED)); + filePath = uh.update(filePath, cursor.getString(INDEX_DATA)); + durationInSec = uh.update( + durationInSec, cursor.getInt(INDEX_DURATION) / 1000); + bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID)); + fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE_ID)); + return uh.isUpdated(); + } + + @Override + public Job requestImage(int type) { + return new LocalVideoRequest(mApplication, getPath(), type, filePath); + } + + public static class LocalVideoRequest extends ImageCacheRequest { + private String mLocalFilePath; + + LocalVideoRequest(GalleryApp application, Path path, int type, + String localFilePath) { + super(application, path, type, LocalImage.getTargetSize(type)); + mLocalFilePath = localFilePath; + } + + @Override + public Bitmap onDecodeOriginal(JobContext jc, int type) { + Bitmap bitmap = BitmapUtils.createVideoThumbnail(mLocalFilePath); + if (bitmap == null || jc.isCancelled()) return null; + return bitmap; + } + } + + @Override + public Job requestLargeImage() { + throw new UnsupportedOperationException("Cannot regquest a large image" + + " to a local video!"); + } + + @Override + public int getSupportedOperations() { + return SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_PLAY | SUPPORT_INFO; + } + + @Override + public void delete() { + GalleryUtils.assertNotInRenderThread(); + Uri baseUri = Video.Media.EXTERNAL_CONTENT_URI; + mApplication.getContentResolver().delete(baseUri, "_id=?", + new String[]{String.valueOf(id)}); + } + + @Override + public void rotate(int degrees) { + // TODO + } + + @Override + public Uri getContentUri() { + Uri baseUri = Video.Media.EXTERNAL_CONTENT_URI; + return baseUri.buildUpon().appendPath(String.valueOf(id)).build(); + } + + @Override + public Uri getPlayUri() { + return Uri.fromFile(new File(filePath)); + } + + @Override + public int getMediaType() { + return MEDIA_TYPE_VIDEO; + } + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + int s = durationInSec; + if (s > 0) { + details.addDetail(MediaDetails.INDEX_DURATION, GalleryUtils.formatDuration( + mApplication.getAndroidContext(), durationInSec)); + } + return details; + } +} diff --git a/src/com/android/gallery3d/data/LocationClustering.java b/src/com/android/gallery3d/data/LocationClustering.java new file mode 100644 index 000000000..3cb1399e5 --- /dev/null +++ b/src/com/android/gallery3d/data/LocationClustering.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.util.ReverseGeocoder; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.Context; +import android.widget.Toast; + +import java.util.ArrayList; + +class LocationClustering extends Clustering { + private static final String TAG = "LocationClustering"; + + private static final int MIN_GROUPS = 1; + private static final int MAX_GROUPS = 20; + private static final int MAX_ITERATIONS = 30; + + // If the total distance change is less than this ratio, stop iterating. + private static final float STOP_CHANGE_RATIO = 0.01f; + private Context mContext; + private ArrayList> mClusters; + private ArrayList mNames; + private String mNoLocationString; + + private static class Point { + public Point(double lat, double lng) { + latRad = Math.toRadians(lat); + lngRad = Math.toRadians(lng); + } + public Point() {} + public double latRad, lngRad; + } + + private static class SmallItem { + Path path; + double lat, lng; + } + + public LocationClustering(Context context) { + mContext = context; + mNoLocationString = mContext.getResources().getString(R.string.no_location); + } + + @Override + public void run(MediaSet baseSet) { + final int total = baseSet.getTotalMediaItemCount(); + final SmallItem[] buf = new SmallItem[total]; + // Separate items to two sets: with or without lat-long. + final double[] latLong = new double[2]; + baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + public void consume(int index, MediaItem item) { + if (index < 0 || index >= total) return; + SmallItem s = new SmallItem(); + s.path = item.getPath(); + item.getLatLong(latLong); + s.lat = latLong[0]; + s.lng = latLong[1]; + buf[index] = s; + } + }); + + final ArrayList withLatLong = new ArrayList(); + final ArrayList withoutLatLong = new ArrayList(); + final ArrayList points = new ArrayList(); + for (int i = 0; i < total; i++) { + SmallItem s = buf[i]; + if (s == null) continue; + if (GalleryUtils.isValidLocation(s.lat, s.lng)) { + withLatLong.add(s); + points.add(new Point(s.lat, s.lng)); + } else { + withoutLatLong.add(s); + } + } + + ArrayList> clusters = new ArrayList>(); + + int m = withLatLong.size(); + if (m > 0) { + // cluster the items with lat-long + Point[] pointsArray = new Point[m]; + pointsArray = points.toArray(pointsArray); + int[] bestK = new int[1]; + int[] index = kMeans(pointsArray, bestK); + + for (int i = 0; i < bestK[0]; i++) { + clusters.add(new ArrayList()); + } + + for (int i = 0; i < m; i++) { + clusters.get(index[i]).add(withLatLong.get(i)); + } + } + + ReverseGeocoder geocoder = new ReverseGeocoder(mContext); + mNames = new ArrayList(); + boolean hasUnresolvedAddress = false; + mClusters = new ArrayList>(); + for (ArrayList cluster : clusters) { + String name = generateName(cluster, geocoder); + if (name != null) { + mNames.add(name); + mClusters.add(cluster); + } else { + // move cluster-i to no location cluster + withoutLatLong.addAll(cluster); + hasUnresolvedAddress = true; + } + } + + if (withoutLatLong.size() > 0) { + mNames.add(mNoLocationString); + mClusters.add(withoutLatLong); + } + + if (hasUnresolvedAddress) { + Toast.makeText(mContext, R.string.no_connectivity, + Toast.LENGTH_LONG).show(); + } + } + + private static String generateName(ArrayList items, + ReverseGeocoder geocoder) { + ReverseGeocoder.SetLatLong set = new ReverseGeocoder.SetLatLong(); + + int n = items.size(); + for (int i = 0; i < n; i++) { + SmallItem item = items.get(i); + double itemLatitude = item.lat; + double itemLongitude = item.lng; + + if (set.mMinLatLatitude > itemLatitude) { + set.mMinLatLatitude = itemLatitude; + set.mMinLatLongitude = itemLongitude; + } + if (set.mMaxLatLatitude < itemLatitude) { + set.mMaxLatLatitude = itemLatitude; + set.mMaxLatLongitude = itemLongitude; + } + if (set.mMinLonLongitude > itemLongitude) { + set.mMinLonLatitude = itemLatitude; + set.mMinLonLongitude = itemLongitude; + } + if (set.mMaxLonLongitude < itemLongitude) { + set.mMaxLonLatitude = itemLatitude; + set.mMaxLonLongitude = itemLongitude; + } + } + + return geocoder.computeAddress(set); + } + + @Override + public int getNumberOfClusters() { + return mClusters.size(); + } + + @Override + public ArrayList getCluster(int index) { + ArrayList items = mClusters.get(index); + ArrayList result = new ArrayList(items.size()); + for (int i = 0, n = items.size(); i < n; i++) { + result.add(items.get(i).path); + } + return result; + } + + @Override + public String getClusterName(int index) { + return mNames.get(index); + } + + // Input: n points + // Output: the best k is stored in bestK[0], and the return value is the + // an array which specifies the group that each point belongs (0 to k - 1). + private static int[] kMeans(Point points[], int[] bestK) { + int n = points.length; + + // min and max number of groups wanted + int minK = Math.min(n, MIN_GROUPS); + int maxK = Math.min(n, MAX_GROUPS); + + Point[] center = new Point[maxK]; // center of each group. + Point[] groupSum = new Point[maxK]; // sum of points in each group. + int[] groupCount = new int[maxK]; // number of points in each group. + int[] grouping = new int[n]; // The group assignment for each point. + + for (int i = 0; i < maxK; i++) { + center[i] = new Point(); + groupSum[i] = new Point(); + } + + // The score we want to minimize is: + // (sum of distance from each point to its group center) * sqrt(k). + float bestScore = Float.MAX_VALUE; + // The best group assignment up to now. + int[] bestGrouping = new int[n]; + // The best K up to now. + bestK[0] = 1; + + float lastDistance = 0; + float totalDistance = 0; + + for (int k = minK; k <= maxK; k++) { + // step 1: (arbitrarily) pick k points as the initial centers. + int delta = n / k; + for (int i = 0; i < k; i++) { + Point p = points[i * delta]; + center[i].latRad = p.latRad; + center[i].lngRad = p.lngRad; + } + + for (int iter = 0; iter < MAX_ITERATIONS; iter++) { + // step 2: assign each point to the nearest center. + for (int i = 0; i < k; i++) { + groupSum[i].latRad = 0; + groupSum[i].lngRad = 0; + groupCount[i] = 0; + } + totalDistance = 0; + + for (int i = 0; i < n; i++) { + Point p = points[i]; + float bestDistance = Float.MAX_VALUE; + int bestIndex = 0; + for (int j = 0; j < k; j++) { + float distance = (float) GalleryUtils.fastDistanceMeters( + p.latRad, p.lngRad, center[j].latRad, center[j].lngRad); + // We may have small non-zero distance introduced by + // floating point calculation, so zero out small + // distances less than 1 meter. + if (distance < 1) { + distance = 0; + } + if (distance < bestDistance) { + bestDistance = distance; + bestIndex = j; + } + } + grouping[i] = bestIndex; + groupCount[bestIndex]++; + groupSum[bestIndex].latRad += p.latRad; + groupSum[bestIndex].lngRad += p.lngRad; + totalDistance += bestDistance; + } + + // step 3: calculate new centers + for (int i = 0; i < k; i++) { + if (groupCount[i] > 0) { + center[i].latRad = groupSum[i].latRad / groupCount[i]; + center[i].lngRad = groupSum[i].lngRad / groupCount[i]; + } + } + + if (totalDistance == 0 || (Math.abs(lastDistance - totalDistance) + / totalDistance) < STOP_CHANGE_RATIO) { + break; + } + lastDistance = totalDistance; + } + + // step 4: remove empty groups and reassign group number + int reassign[] = new int[k]; + int realK = 0; + for (int i = 0; i < k; i++) { + if (groupCount[i] > 0) { + reassign[i] = realK++; + } + } + + // step 5: calculate the final score + float score = totalDistance * (float) Math.sqrt(realK); + + if (score < bestScore) { + bestScore = score; + bestK[0] = realK; + for (int i = 0; i < n; i++) { + bestGrouping[i] = reassign[grouping[i]]; + } + if (score == 0) { + break; + } + } + } + return bestGrouping; + } +} diff --git a/src/com/android/gallery3d/data/Log.java b/src/com/android/gallery3d/data/Log.java new file mode 100644 index 000000000..3384eb66c --- /dev/null +++ b/src/com/android/gallery3d/data/Log.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +public class Log { + public static int v(String tag, String msg) { + return android.util.Log.v(tag, msg); + } + public static int v(String tag, String msg, Throwable tr) { + return android.util.Log.v(tag, msg, tr); + } + public static int d(String tag, String msg) { + return android.util.Log.d(tag, msg); + } + public static int d(String tag, String msg, Throwable tr) { + return android.util.Log.d(tag, msg, tr); + } + public static int i(String tag, String msg) { + return android.util.Log.i(tag, msg); + } + public static int i(String tag, String msg, Throwable tr) { + return android.util.Log.i(tag, msg, tr); + } + public static int w(String tag, String msg) { + return android.util.Log.w(tag, msg); + } + public static int w(String tag, String msg, Throwable tr) { + return android.util.Log.w(tag, msg, tr); + } + public static int w(String tag, Throwable tr) { + return android.util.Log.w(tag, tr); + } + public static int e(String tag, String msg) { + return android.util.Log.e(tag, msg); + } + public static int e(String tag, String msg, Throwable tr) { + return android.util.Log.e(tag, msg, tr); + } +} diff --git a/src/com/android/gallery3d/data/MediaDetails.java b/src/com/android/gallery3d/data/MediaDetails.java new file mode 100644 index 000000000..1b56ac42e --- /dev/null +++ b/src/com/android/gallery3d/data/MediaDetails.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.R; + +import android.media.ExifInterface; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.TreeMap; +import java.util.Map.Entry; + +public class MediaDetails implements Iterable> { + @SuppressWarnings("unused") + private static final String TAG = "MediaDetails"; + + private TreeMap mDetails = new TreeMap(); + private HashMap mUnits = new HashMap(); + + public static final int INDEX_TITLE = 1; + public static final int INDEX_DESCRIPTION = 2; + public static final int INDEX_DATETIME = 3; + public static final int INDEX_LOCATION = 4; + public static final int INDEX_WIDTH = 5; + public static final int INDEX_HEIGHT = 6; + public static final int INDEX_ORIENTATION = 7; + public static final int INDEX_DURATION = 8; + public static final int INDEX_MIMETYPE = 9; + public static final int INDEX_SIZE = 10; + + // for EXIF + public static final int INDEX_MAKE = 100; + public static final int INDEX_MODEL = 101; + public static final int INDEX_FLASH = 102; + public static final int INDEX_FOCAL_LENGTH = 103; + public static final int INDEX_WHITE_BALANCE = 104; + public static final int INDEX_APERTURE = 105; + public static final int INDEX_SHUTTER_SPEED = 106; + public static final int INDEX_EXPOSURE_TIME = 107; + public static final int INDEX_ISO = 108; + + // Put this last because it may be long. + public static final int INDEX_PATH = 200; + + public static class FlashState { + private static int FLASH_FIRED_MASK = 1; + private static int FLASH_RETURN_MASK = 2 | 4; + private static int FLASH_MODE_MASK = 8 | 16; + private static int FLASH_FUNCTION_MASK = 32; + private static int FLASH_RED_EYE_MASK = 64; + private int mState; + + public FlashState(int state) { + mState = state; + } + + public boolean isFlashFired() { + return (mState & FLASH_FIRED_MASK) != 0; + } + + public int getFlashReturn() { + return (mState & FLASH_RETURN_MASK) >> 1; + } + + public int getFlashMode() { + return (mState & FLASH_MODE_MASK) >> 3; + } + + public boolean isFlashPresent() { + return (mState & FLASH_FUNCTION_MASK) != 0; + } + + public boolean isRedEyeModePresent() { + return (mState & FLASH_RED_EYE_MASK) != 0; + } + } + + public void addDetail(int index, Object value) { + mDetails.put(index, value); + } + + public Object getDetail(int index) { + return mDetails.get(index); + } + + public int size() { + return mDetails.size(); + } + + public Iterator> iterator() { + return mDetails.entrySet().iterator(); + } + + public void setUnit(int index, int unit) { + mUnits.put(index, unit); + } + + public boolean hasUnit(int index) { + return mUnits.containsKey(index); + } + + public int getUnit(int index) { + return mUnits.get(index); + } + + private static void setExifData(MediaDetails details, ExifInterface exif, String tag, + int key) { + String value = exif.getAttribute(tag); + if (value != null) { + if (key == MediaDetails.INDEX_FLASH) { + MediaDetails.FlashState state = new MediaDetails.FlashState( + Integer.valueOf(value.toString())); + details.addDetail(key, state); + } else { + details.addDetail(key, value); + } + } + } + + public static void extractExifInfo(MediaDetails details, String filePath) { + try { + ExifInterface exif = new ExifInterface(filePath); + setExifData(details, exif, ExifInterface.TAG_FLASH, MediaDetails.INDEX_FLASH); + setExifData(details, exif, ExifInterface.TAG_IMAGE_WIDTH, MediaDetails.INDEX_WIDTH); + setExifData(details, exif, ExifInterface.TAG_IMAGE_LENGTH, + MediaDetails.INDEX_HEIGHT); + setExifData(details, exif, ExifInterface.TAG_MAKE, MediaDetails.INDEX_MAKE); + setExifData(details, exif, ExifInterface.TAG_MODEL, MediaDetails.INDEX_MODEL); + setExifData(details, exif, ExifInterface.TAG_APERTURE, MediaDetails.INDEX_APERTURE); + setExifData(details, exif, ExifInterface.TAG_ISO, MediaDetails.INDEX_ISO); + setExifData(details, exif, ExifInterface.TAG_WHITE_BALANCE, + MediaDetails.INDEX_WHITE_BALANCE); + setExifData(details, exif, ExifInterface.TAG_EXPOSURE_TIME, + MediaDetails.INDEX_EXPOSURE_TIME); + + double data = exif.getAttributeDouble(ExifInterface.TAG_FOCAL_LENGTH, 0); + if (data != 0f) { + details.addDetail(MediaDetails.INDEX_FOCAL_LENGTH, data); + details.setUnit(MediaDetails.INDEX_FOCAL_LENGTH, R.string.unit_mm); + } + } catch (IOException ex) { + // ignore it. + Log.w(TAG, "", ex); + } + } +} diff --git a/src/com/android/gallery3d/data/MediaItem.java b/src/com/android/gallery3d/data/MediaItem.java new file mode 100644 index 000000000..430d8327d --- /dev/null +++ b/src/com/android/gallery3d/data/MediaItem.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.util.ThreadPool.Job; + +import android.graphics.Bitmap; +import android.graphics.BitmapRegionDecoder; + +// MediaItem represents an image or a video item. +public abstract class MediaItem extends MediaObject { + // NOTE: These type numbers are stored in the image cache, so it should not + // not be changed without resetting the cache. + public static final int TYPE_THUMBNAIL = 1; + public static final int TYPE_MICROTHUMBNAIL = 2; + + public static final int IMAGE_READY = 0; + public static final int IMAGE_WAIT = 1; + public static final int IMAGE_ERROR = -1; + + // TODO: fix default value for latlng and change this. + public static final double INVALID_LATLNG = 0f; + + public abstract Job requestImage(int type); + public abstract Job requestLargeImage(); + + public MediaItem(Path path, long version) { + super(path, version); + } + + public long getDateInMs() { + return 0; + } + + public String getName() { + return null; + } + + public void getLatLong(double[] latLong) { + latLong[0] = INVALID_LATLNG; + latLong[1] = INVALID_LATLNG; + } + + public String[] getTags() { + return null; + } + + public Face[] getFaces() { + return null; + } + + public int getRotation() { + return 0; + } + + public long getSize() { + return 0; + } + + public abstract String getMimeType(); +} diff --git a/src/com/android/gallery3d/data/MediaObject.java b/src/com/android/gallery3d/data/MediaObject.java new file mode 100644 index 000000000..d0f1672fc --- /dev/null +++ b/src/com/android/gallery3d/data/MediaObject.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import android.net.Uri; + +public abstract class MediaObject { + @SuppressWarnings("unused") + private static final String TAG = "MediaObject"; + public static final long INVALID_DATA_VERSION = -1; + + // These are the bits returned from getSupportedOperations(): + public static final int SUPPORT_DELETE = 1 << 0; + public static final int SUPPORT_ROTATE = 1 << 1; + public static final int SUPPORT_SHARE = 1 << 2; + public static final int SUPPORT_CROP = 1 << 3; + public static final int SUPPORT_SHOW_ON_MAP = 1 << 4; + public static final int SUPPORT_SETAS = 1 << 5; + public static final int SUPPORT_FULL_IMAGE = 1 << 6; + public static final int SUPPORT_PLAY = 1 << 7; + public static final int SUPPORT_CACHE = 1 << 8; + public static final int SUPPORT_EDIT = 1 << 9; + public static final int SUPPORT_INFO = 1 << 10; + public static final int SUPPORT_IMPORT = 1 << 11; + public static final int SUPPORT_ALL = 0xffffffff; + + // These are the bits returned from getMediaType(): + public static final int MEDIA_TYPE_UNKNOWN = 1; + public static final int MEDIA_TYPE_IMAGE = 2; + public static final int MEDIA_TYPE_VIDEO = 4; + public static final int MEDIA_TYPE_ALL = MEDIA_TYPE_IMAGE | MEDIA_TYPE_VIDEO; + + // These are flags for cache() and return values for getCacheFlag(): + public static final int CACHE_FLAG_NO = 0; + public static final int CACHE_FLAG_SCREENNAIL = 1; + public static final int CACHE_FLAG_FULL = 2; + + // These are return values for getCacheStatus(): + public static final int CACHE_STATUS_NOT_CACHED = 0; + public static final int CACHE_STATUS_CACHING = 1; + public static final int CACHE_STATUS_CACHED_SCREENNAIL = 2; + public static final int CACHE_STATUS_CACHED_FULL = 3; + + private static long sVersionSerial = 0; + + protected long mDataVersion; + + protected final Path mPath; + + public MediaObject(Path path, long version) { + path.setObject(this); + mPath = path; + mDataVersion = version; + } + + public Path getPath() { + return mPath; + } + + public int getSupportedOperations() { + return 0; + } + + public void delete() { + throw new UnsupportedOperationException(); + } + + public void rotate(int degrees) { + throw new UnsupportedOperationException(); + } + + public Uri getContentUri() { + throw new UnsupportedOperationException(); + } + + public Uri getPlayUri() { + throw new UnsupportedOperationException(); + } + + public int getMediaType() { + return MEDIA_TYPE_UNKNOWN; + } + + public boolean Import() { + throw new UnsupportedOperationException(); + } + + public MediaDetails getDetails() { + MediaDetails details = new MediaDetails(); + return details; + } + + public long getDataVersion() { + return mDataVersion; + } + + public int getCacheFlag() { + return CACHE_FLAG_NO; + } + + public int getCacheStatus() { + throw new UnsupportedOperationException(); + } + + public long getCacheSize() { + throw new UnsupportedOperationException(); + } + + public void cache(int flag) { + throw new UnsupportedOperationException(); + } + + public static synchronized long nextVersionNumber() { + return ++MediaObject.sVersionSerial; + } +} diff --git a/src/com/android/gallery3d/data/MediaSet.java b/src/com/android/gallery3d/data/MediaSet.java new file mode 100644 index 000000000..99f00a0dd --- /dev/null +++ b/src/com/android/gallery3d/data/MediaSet.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.util.Future; + +import java.util.ArrayList; +import java.util.WeakHashMap; + +// MediaSet is a directory-like data structure. +// It contains MediaItems and sub-MediaSets. +// +// The primary interface are: +// getMediaItemCount(), getMediaItem() and +// getSubMediaSetCount(), getSubMediaSet(). +// +// getTotalMediaItemCount() returns the number of all MediaItems, including +// those in sub-MediaSets. +public abstract class MediaSet extends MediaObject { + public static final int MEDIAITEM_BATCH_FETCH_COUNT = 500; + public static final int INDEX_NOT_FOUND = -1; + + public MediaSet(Path path, long version) { + super(path, version); + } + + public int getMediaItemCount() { + return 0; + } + + // Returns the media items in the range [start, start + count). + // + // The number of media items returned may be less than the specified count + // if there are not enough media items available. The number of + // media items available may not be consistent with the return value of + // getMediaItemCount() because the contents of database may have already + // changed. + public ArrayList getMediaItem(int start, int count) { + return new ArrayList(); + } + + public int getSubMediaSetCount() { + return 0; + } + + public MediaSet getSubMediaSet(int index) { + throw new IndexOutOfBoundsException(); + } + + public boolean isLeafAlbum() { + return false; + } + + public int getTotalMediaItemCount() { + int total = getMediaItemCount(); + for (int i = 0, n = getSubMediaSetCount(); i < n; i++) { + total += getSubMediaSet(i).getTotalMediaItemCount(); + } + return total; + } + + // TODO: we should have better implementation of sub classes + public int getIndexOfItem(Path path, int hint) { + // hint < 0 is handled below + // first, try to find it around the hint + int start = Math.max(0, + hint - MEDIAITEM_BATCH_FETCH_COUNT / 2); + ArrayList list = getMediaItem( + start, MEDIAITEM_BATCH_FETCH_COUNT); + int index = getIndexOf(path, list); + if (index != INDEX_NOT_FOUND) return start + index; + + // try to find it globally + start = start == 0 ? MEDIAITEM_BATCH_FETCH_COUNT : 0; + list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT); + while (true) { + index = getIndexOf(path, list); + if (index != INDEX_NOT_FOUND) return start + index; + if (list.size() < MEDIAITEM_BATCH_FETCH_COUNT) return INDEX_NOT_FOUND; + start += MEDIAITEM_BATCH_FETCH_COUNT; + list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT); + } + } + + protected int getIndexOf(Path path, ArrayList list) { + for (int i = 0, n = list.size(); i < n; ++i) { + if (list.get(i).mPath == path) return i; + } + return INDEX_NOT_FOUND; + } + + public abstract String getName(); + + private WeakHashMap mListeners = + new WeakHashMap(); + + // NOTE: The MediaSet only keeps a weak reference to the listener. The + // listener is automatically removed when there is no other reference to + // the listener. + public void addContentListener(ContentListener listener) { + if (mListeners.containsKey(listener)) { + throw new IllegalArgumentException(); + } + mListeners.put(listener, null); + } + + public void removeContentListener(ContentListener listener) { + if (!mListeners.containsKey(listener)) { + throw new IllegalArgumentException(); + } + mListeners.remove(listener); + } + + // This should be called by subclasses when the content is changed. + public void notifyContentChanged() { + for (ContentListener listener : mListeners.keySet()) { + listener.onContentDirty(); + } + } + + // Reload the content. Return the current data version. reload() should be called + // in the same thread as getMediaItem(int, int) and getSubMediaSet(int). + public abstract long reload(); + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + details.addDetail(MediaDetails.INDEX_TITLE, getName()); + return details; + } + + // Enumerate all media items in this media set (including the ones in sub + // media sets), in an efficient order. ItemConsumer.consumer() will be + // called for each media item with its index. + public void enumerateMediaItems(ItemConsumer consumer) { + enumerateMediaItems(consumer, 0); + } + + public void enumerateTotalMediaItems(ItemConsumer consumer) { + enumerateTotalMediaItems(consumer, 0); + } + + public static interface ItemConsumer { + void consume(int index, MediaItem item); + } + + // The default implementation uses getMediaItem() for enumerateMediaItems(). + // Subclasses may override this and use more efficient implementations. + // Returns the number of items enumerated. + protected int enumerateMediaItems(ItemConsumer consumer, int startIndex) { + int total = getMediaItemCount(); + int start = 0; + while (start < total) { + int count = Math.min(MEDIAITEM_BATCH_FETCH_COUNT, total - start); + ArrayList items = getMediaItem(start, count); + for (int i = 0, n = items.size(); i < n; i++) { + MediaItem item = items.get(i); + consumer.consume(startIndex + start + i, item); + } + start += count; + } + return total; + } + + // Recursively enumerate all media items under this set. + // Returns the number of items enumerated. + protected int enumerateTotalMediaItems( + ItemConsumer consumer, int startIndex) { + int start = 0; + start += enumerateMediaItems(consumer, startIndex); + int m = getSubMediaSetCount(); + for (int i = 0; i < m; i++) { + start += getSubMediaSet(i).enumerateTotalMediaItems( + consumer, startIndex + start); + } + return start; + } + + public Future requestSync() { + return FUTURE_STUB; + } + + private static final Future FUTURE_STUB = new Future() { + @Override + public void cancel() {} + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public Void get() { + return null; + } + + @Override + public void waitDone() {} + }; +} diff --git a/src/com/android/gallery3d/data/MediaSource.java b/src/com/android/gallery3d/data/MediaSource.java new file mode 100644 index 000000000..ae98e0fcc --- /dev/null +++ b/src/com/android/gallery3d/data/MediaSource.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.data.MediaSet.ItemConsumer; + +import android.net.Uri; + +import java.util.ArrayList; + +public abstract class MediaSource { + private static final String TAG = "MediaSource"; + private String mPrefix; + + protected MediaSource(String prefix) { + mPrefix = prefix; + } + + public String getPrefix() { + return mPrefix; + } + + public Path findPathByUri(Uri uri) { + return null; + } + + public abstract MediaObject createMediaObject(Path path); + + public void pause() { + } + + public void resume() { + } + + public Path getDefaultSetOf(Path item) { + return null; + } + + public long getTotalUsedCacheSize() { + return 0; + } + + public long getTotalTargetCacheSize() { + return 0; + } + + public static class PathId { + public PathId(Path path, int id) { + this.path = path; + this.id = id; + } + public Path path; + public int id; + } + + // Maps a list of Paths (all belong to this MediaSource) to MediaItems, + // and invoke consumer.consume() for each MediaItem with the given id. + // + // This default implementation uses getMediaObject for each Path. Subclasses + // may override this and provide more efficient implementation (like + // batching the database query). + public void mapMediaItems(ArrayList list, ItemConsumer consumer) { + int n = list.size(); + for (int i = 0; i < n; i++) { + PathId pid = list.get(i); + MediaObject obj = pid.path.getObject(); + if (obj == null) { + try { + obj = createMediaObject(pid.path); + } catch (Throwable th) { + Log.w(TAG, "cannot create media object: " + pid.path, th); + } + } + if (obj != null) { + consumer.consume(pid.id, (MediaItem) obj); + } + } + } +} diff --git a/src/com/android/gallery3d/data/MtpClient.java b/src/com/android/gallery3d/data/MtpClient.java new file mode 100644 index 000000000..6991c1637 --- /dev/null +++ b/src/com/android/gallery3d/data/MtpClient.java @@ -0,0 +1,442 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbInterface; +import android.hardware.usb.UsbManager; +import android.mtp.MtpDevice; +import android.mtp.MtpDeviceInfo; +import android.mtp.MtpObjectInfo; +import android.mtp.MtpStorageInfo; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * This class helps an application manage a list of connected MTP or PTP devices. + * It listens for MTP devices being attached and removed from the USB host bus + * and notifies the application when the MTP device list changes. + */ +public class MtpClient { + + private static final String TAG = "MtpClient"; + + private static final String ACTION_USB_PERMISSION = + "android.mtp.MtpClient.action.USB_PERMISSION"; + + private final Context mContext; + private final UsbManager mUsbManager; + private final ArrayList mListeners = new ArrayList(); + // mDevices contains all MtpDevices that have been seen by our client, + // so we can inform when the device has been detached. + // mDevices is also used for synchronization in this class. + private final HashMap mDevices = new HashMap(); + // List of MTP devices we should not try to open for which we are currently + // asking for permission to open. + private final ArrayList mRequestPermissionDevices = new ArrayList(); + // List of MTP devices we should not try to open. + // We add devices to this list if the user canceled a permission request or we were + // unable to open the device. + private final ArrayList mIgnoredDevices = new ArrayList(); + + private final PendingIntent mPermissionIntent; + + private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + UsbDevice usbDevice = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + String deviceName = usbDevice.getDeviceName(); + + synchronized (mDevices) { + MtpDevice mtpDevice = mDevices.get(deviceName); + + if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { + if (mtpDevice == null) { + mtpDevice = openDeviceLocked(usbDevice); + } + if (mtpDevice != null) { + for (Listener listener : mListeners) { + listener.deviceAdded(mtpDevice); + } + } + } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { + if (mtpDevice != null) { + mDevices.remove(deviceName); + mRequestPermissionDevices.remove(deviceName); + mIgnoredDevices.remove(deviceName); + for (Listener listener : mListeners) { + listener.deviceRemoved(mtpDevice); + } + } + } else if (ACTION_USB_PERMISSION.equals(action)) { + mRequestPermissionDevices.remove(deviceName); + boolean permission = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, + false); + Log.d(TAG, "ACTION_USB_PERMISSION: " + permission); + if (permission) { + if (mtpDevice == null) { + mtpDevice = openDeviceLocked(usbDevice); + } + if (mtpDevice != null) { + for (Listener listener : mListeners) { + listener.deviceAdded(mtpDevice); + } + } + } else { + // so we don't ask for permission again + mIgnoredDevices.add(deviceName); + } + } + } + } + }; + + /** + * An interface for being notified when MTP or PTP devices are attached + * or removed. In the current implementation, only PTP devices are supported. + */ + public interface Listener { + /** + * Called when a new device has been added + * + * @param device the new device that was added + */ + public void deviceAdded(MtpDevice device); + + /** + * Called when a new device has been removed + * + * @param device the device that was removed + */ + public void deviceRemoved(MtpDevice device); + } + + /** + * Tests to see if a {@link android.hardware.usb.UsbDevice} + * supports the PTP protocol (typically used by digital cameras) + * + * @param device the device to test + * @return true if the device is a PTP device. + */ + static public boolean isCamera(UsbDevice device) { + int count = device.getInterfaceCount(); + for (int i = 0; i < count; i++) { + UsbInterface intf = device.getInterface(i); + if (intf.getInterfaceClass() == UsbConstants.USB_CLASS_STILL_IMAGE && + intf.getInterfaceSubclass() == 1 && + intf.getInterfaceProtocol() == 1) { + return true; + } + } + return false; + } + + /** + * MtpClient constructor + * + * @param context the {@link android.content.Context} to use for the MtpClient + */ + public MtpClient(Context context) { + mContext = context; + mUsbManager = (UsbManager)context.getSystemService(Context.USB_SERVICE); + mPermissionIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_USB_PERMISSION), 0); + IntentFilter filter = new IntentFilter(); + filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); + filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); + filter.addAction(ACTION_USB_PERMISSION); + context.registerReceiver(mUsbReceiver, filter); + } + + /** + * Opens the {@link android.hardware.usb.UsbDevice} for an MTP or PTP + * device and return an {@link android.mtp.MtpDevice} for it. + * + * @param device the device to open + * @return an MtpDevice for the device. + */ + private MtpDevice openDeviceLocked(UsbDevice usbDevice) { + String deviceName = usbDevice.getDeviceName(); + + // don't try to open devices that we have decided to ignore + // or are currently asking permission for + if (isCamera(usbDevice) && !mIgnoredDevices.contains(deviceName) + && !mRequestPermissionDevices.contains(deviceName)) { + if (!mUsbManager.hasPermission(usbDevice)) { + mUsbManager.requestPermission(usbDevice, mPermissionIntent); + mRequestPermissionDevices.add(deviceName); + } else { + UsbDeviceConnection connection = mUsbManager.openDevice(usbDevice); + if (connection != null) { + MtpDevice mtpDevice = new MtpDevice(usbDevice); + if (mtpDevice.open(connection)) { + mDevices.put(usbDevice.getDeviceName(), mtpDevice); + return mtpDevice; + } else { + // so we don't try to open it again + mIgnoredDevices.add(deviceName); + } + } else { + // so we don't try to open it again + mIgnoredDevices.add(deviceName); + } + } + } + return null; + } + + /** + * Closes all resources related to the MtpClient object + */ + public void close() { + mContext.unregisterReceiver(mUsbReceiver); + } + + /** + * Registers a {@link android.mtp.MtpClient.Listener} interface to receive + * notifications when MTP or PTP devices are added or removed. + * + * @param listener the listener to register + */ + public void addListener(Listener listener) { + synchronized (mDevices) { + if (!mListeners.contains(listener)) { + mListeners.add(listener); + } + } + } + + /** + * Unregisters a {@link android.mtp.MtpClient.Listener} interface. + * + * @param listener the listener to unregister + */ + public void removeListener(Listener listener) { + synchronized (mDevices) { + mListeners.remove(listener); + } + } + + /** + * Retrieves an {@link android.mtp.MtpDevice} object for the USB device + * with the given name. + * + * @param deviceName the name of the USB device + * @return the MtpDevice, or null if it does not exist + */ + public MtpDevice getDevice(String deviceName) { + synchronized (mDevices) { + return mDevices.get(deviceName); + } + } + + /** + * Retrieves an {@link android.mtp.MtpDevice} object for the USB device + * with the given ID. + * + * @param id the ID of the USB device + * @return the MtpDevice, or null if it does not exist + */ + public MtpDevice getDevice(int id) { + synchronized (mDevices) { + return mDevices.get(UsbDevice.getDeviceName(id)); + } + } + + /** + * Retrieves a list of all currently connected {@link android.mtp.MtpDevice}. + * + * @return the list of MtpDevices + */ + public List getDeviceList() { + synchronized (mDevices) { + // Query the USB manager since devices might have attached + // before we added our listener. + for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) { + if (mDevices.get(usbDevice.getDeviceName()) == null) { + openDeviceLocked(usbDevice); + } + } + + return new ArrayList(mDevices.values()); + } + } + + /** + * Retrieves a list of all {@link android.mtp.MtpStorageInfo} + * for the MTP or PTP device with the given USB device name + * + * @param deviceName the name of the USB device + * @return the list of MtpStorageInfo + */ + public List getStorageList(String deviceName) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return null; + } + int[] storageIds = device.getStorageIds(); + if (storageIds == null) { + return null; + } + + int length = storageIds.length; + ArrayList storageList = new ArrayList(length); + for (int i = 0; i < length; i++) { + MtpStorageInfo info = device.getStorageInfo(storageIds[i]); + if (info == null) { + Log.w(TAG, "getStorageInfo failed"); + } else { + storageList.add(info); + } + } + return storageList; + } + + /** + * Retrieves the {@link android.mtp.MtpObjectInfo} for an object on + * the MTP or PTP device with the given USB device name with the given + * object handle + * + * @param deviceName the name of the USB device + * @param objectHandle handle of the object to query + * @return the MtpObjectInfo + */ + public MtpObjectInfo getObjectInfo(String deviceName, int objectHandle) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return null; + } + return device.getObjectInfo(objectHandle); + } + + /** + * Deletes an object on the MTP or PTP device with the given USB device name. + * + * @param deviceName the name of the USB device + * @param objectHandle handle of the object to delete + * @return true if the deletion succeeds + */ + public boolean deleteObject(String deviceName, int objectHandle) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return false; + } + return device.deleteObject(objectHandle); + } + + /** + * Retrieves a list of {@link android.mtp.MtpObjectInfo} for all objects + * on the MTP or PTP device with the given USB device name and given storage ID + * and/or object handle. + * If the object handle is zero, then all objects in the root of the storage unit + * will be returned. Otherwise, all immediate children of the object will be returned. + * If the storage ID is also zero, then all objects on all storage units will be returned. + * + * @param deviceName the name of the USB device + * @param storageId the ID of the storage unit to query, or zero for all + * @param objectHandle the handle of the parent object to query, or zero for the storage root + * @return the list of MtpObjectInfo + */ + public List getObjectList(String deviceName, int storageId, int objectHandle) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return null; + } + if (objectHandle == 0) { + // all objects in root of storage + objectHandle = 0xFFFFFFFF; + } + int[] handles = device.getObjectHandles(storageId, 0, objectHandle); + if (handles == null) { + return null; + } + + int length = handles.length; + ArrayList objectList = new ArrayList(length); + for (int i = 0; i < length; i++) { + MtpObjectInfo info = device.getObjectInfo(handles[i]); + if (info == null) { + Log.w(TAG, "getObjectInfo failed"); + } else { + objectList.add(info); + } + } + return objectList; + } + + /** + * Returns the data for an object as a byte array. + * + * @param deviceName the name of the USB device containing the object + * @param objectHandle handle of the object to read + * @param objectSize the size of the object (this should match + * {@link android.mtp.MtpObjectInfo#getCompressedSize} + * @return the object's data, or null if reading fails + */ + public byte[] getObject(String deviceName, int objectHandle, int objectSize) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return null; + } + return device.getObject(objectHandle, objectSize); + } + + /** + * Returns the thumbnail data for an object as a byte array. + * + * @param deviceName the name of the USB device containing the object + * @param objectHandle handle of the object to read + * @return the object's thumbnail, or null if reading fails + */ + public byte[] getThumbnail(String deviceName, int objectHandle) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return null; + } + return device.getThumbnail(objectHandle); + } + + /** + * Copies the data for an object to a file in external storage. + * + * @param deviceName the name of the USB device containing the object + * @param objectHandle handle of the object to read + * @param destPath path to destination for the file transfer. + * This path should be in the external storage as defined by + * {@link android.os.Environment#getExternalStorageDirectory} + * @return true if the file transfer succeeds + */ + public boolean importFile(String deviceName, int objectHandle, String destPath) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return false; + } + return device.importFile(objectHandle, destPath); + } +} diff --git a/src/com/android/gallery3d/data/MtpContext.java b/src/com/android/gallery3d/data/MtpContext.java new file mode 100644 index 000000000..652849445 --- /dev/null +++ b/src/com/android/gallery3d/data/MtpContext.java @@ -0,0 +1,141 @@ +package com.android.gallery3d.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.Context; +import android.hardware.usb.UsbDevice; +import android.media.MediaScannerConnection; +import android.media.MediaScannerConnection.MediaScannerConnectionClient; +import android.mtp.MtpObjectInfo; +import android.net.Uri; +import android.os.Environment; +import android.util.Log; +import android.widget.Toast; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class MtpContext implements MtpClient.Listener { + private static final String TAG = "MtpContext"; + + public static final String NAME_IMPORTED_FOLDER = "Imported"; + + private ScannerClient mScannerClient; + private Context mContext; + private MtpClient mClient; + + private static final class ScannerClient implements MediaScannerConnectionClient { + ArrayList mPaths = new ArrayList(); + MediaScannerConnection mScannerConnection; + boolean mConnected; + Object mLock = new Object(); + + public ScannerClient(Context context) { + mScannerConnection = new MediaScannerConnection(context, this); + } + + public void scanPath(String path) { + synchronized (mLock) { + if (mConnected) { + mScannerConnection.scanFile(path, null); + } else { + mPaths.add(path); + mScannerConnection.connect(); + } + } + } + + @Override + public void onMediaScannerConnected() { + synchronized (mLock) { + mConnected = true; + if (!mPaths.isEmpty()) { + for (String path : mPaths) { + mScannerConnection.scanFile(path, null); + } + mPaths.clear(); + } + } + } + + @Override + public void onScanCompleted(String path, Uri uri) { + } + } + + public MtpContext(Context context) { + mContext = context; + mScannerClient = new ScannerClient(context); + mClient = new MtpClient(mContext); + } + + public void pause() { + mClient.removeListener(this); + } + + public void resume() { + mClient.addListener(this); + notifyDirty(); + } + + public void deviceAdded(android.mtp.MtpDevice device) { + notifyDirty(); + showToast(R.string.camera_connected); + } + + public void deviceRemoved(android.mtp.MtpDevice device) { + notifyDirty(); + showToast(R.string.camera_disconnected); + } + + private void notifyDirty() { + mContext.getContentResolver().notifyChange(Uri.parse("mtp://"), null); + } + + private void showToast(final int msg) { + Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show(); + } + + public MtpClient getMtpClient() { + return mClient; + } + + public boolean copyFile(String deviceName, MtpObjectInfo objInfo) { + if (GalleryUtils.hasSpaceForSize(objInfo.getCompressedSize())) { + File dest = Environment.getExternalStorageDirectory(); + dest = new File(dest, NAME_IMPORTED_FOLDER); + dest.mkdirs(); + String destPath = new File(dest, objInfo.getName()).getAbsolutePath(); + int objectId = objInfo.getObjectHandle(); + if (mClient.importFile(deviceName, objectId, destPath)) { + mScannerClient.scanPath(destPath); + return true; + } + } else { + Log.w(TAG, "No space to import " + objInfo.getName() + + " whose size = " + objInfo.getCompressedSize()); + } + return false; + } + + public boolean copyAlbum(String deviceName, String albumName, + List children) { + File dest = Environment.getExternalStorageDirectory(); + dest = new File(dest, albumName); + dest.mkdirs(); + int success = 0; + for (MtpObjectInfo child : children) { + if (!GalleryUtils.hasSpaceForSize(child.getCompressedSize())) continue; + + File importedFile = new File(dest, child.getName()); + String path = importedFile.getAbsolutePath(); + if (mClient.importFile(deviceName, child.getObjectHandle(), path)) { + mScannerClient.scanPath(path); + success++; + } + } + return success == children.size(); + } +} diff --git a/src/com/android/gallery3d/data/MtpDevice.java b/src/com/android/gallery3d/data/MtpDevice.java new file mode 100644 index 000000000..e654583c5 --- /dev/null +++ b/src/com/android/gallery3d/data/MtpDevice.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.GalleryApp; + +import android.hardware.usb.UsbDevice; +import android.mtp.MtpConstants; +import android.mtp.MtpObjectInfo; +import android.mtp.MtpStorageInfo; +import android.net.Uri; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +public class MtpDevice extends MediaSet { + private static final String TAG = "MtpDevice"; + + private final GalleryApp mApplication; + private final int mDeviceId; + private final String mDeviceName; + private final DataManager mDataManager; + private final MtpContext mMtpContext; + private final String mName; + private final ChangeNotifier mNotifier; + private final Path mItemPath; + private List mJpegChildren; + + public MtpDevice(Path path, GalleryApp application, int deviceId, + String name, MtpContext mtpContext) { + super(path, nextVersionNumber()); + mApplication = application; + mDeviceId = deviceId; + mDeviceName = UsbDevice.getDeviceName(deviceId); + mDataManager = application.getDataManager(); + mMtpContext = mtpContext; + mName = name; + mNotifier = new ChangeNotifier(this, Uri.parse("mtp://"), application); + mItemPath = Path.fromString("/mtp/item/" + String.valueOf(deviceId)); + mJpegChildren = new ArrayList(); + } + + public MtpDevice(Path path, GalleryApp application, int deviceId, + MtpContext mtpContext) { + this(path, application, deviceId, + MtpDeviceSet.getDeviceName(mtpContext, deviceId), mtpContext); + } + + private List loadItems() { + ArrayList result = new ArrayList(); + + List storageList = mMtpContext.getMtpClient() + .getStorageList(mDeviceName); + if (storageList == null) return result; + + for (MtpStorageInfo info : storageList) { + collectJpegChildren(info.getStorageId(), 0, result); + } + + return result; + } + + private void collectJpegChildren(int storageId, int objectId, + ArrayList result) { + ArrayList dirChildren = new ArrayList(); + + queryChildren(storageId, objectId, result, dirChildren); + + for (int i = 0, n = dirChildren.size(); i < n; i++) { + MtpObjectInfo info = dirChildren.get(i); + collectJpegChildren(storageId, info.getObjectHandle(), result); + } + } + + private void queryChildren(int storageId, int objectId, + ArrayList jpeg, ArrayList dir) { + List children = mMtpContext.getMtpClient().getObjectList( + mDeviceName, storageId, objectId); + if (children == null) return; + + for (MtpObjectInfo obj : children) { + int format = obj.getFormat(); + switch (format) { + case MtpConstants.FORMAT_JFIF: + case MtpConstants.FORMAT_EXIF_JPEG: + jpeg.add(obj); + break; + case MtpConstants.FORMAT_ASSOCIATION: + dir.add(obj); + break; + default: + Log.w(TAG, "other type: name = " + obj.getName() + + ", format = " + format); + } + } + } + + public static MtpObjectInfo getObjectInfo(MtpContext mtpContext, int deviceId, + int objectId) { + String deviceName = UsbDevice.getDeviceName(deviceId); + return mtpContext.getMtpClient().getObjectInfo(deviceName, objectId); + } + + @Override + public ArrayList getMediaItem(int start, int count) { + ArrayList result = new ArrayList(); + int begin = start; + int end = Math.min(start + count, mJpegChildren.size()); + + DataManager dataManager = mApplication.getDataManager(); + for (int i = begin; i < end; i++) { + MtpObjectInfo child = mJpegChildren.get(i); + Path childPath = mItemPath.getChild(child.getObjectHandle()); + MtpImage image = (MtpImage) dataManager.peekMediaObject(childPath); + if (image == null) { + image = new MtpImage( + childPath, mApplication, mDeviceId, child, mMtpContext); + } else { + image.updateContent(child); + } + result.add(image); + } + return result; + } + + @Override + public int getMediaItemCount() { + return mJpegChildren.size(); + } + + @Override + public String getName() { + return mName; + } + + @Override + public long reload() { + if (mNotifier.isDirty()) { + mDataVersion = nextVersionNumber(); + mJpegChildren = loadItems(); + } + return mDataVersion; + } + + @Override + public int getSupportedOperations() { + return SUPPORT_IMPORT; + } + + @Override + public boolean Import() { + return mMtpContext.copyAlbum(mDeviceName, mName, mJpegChildren); + } + + @Override + public boolean isLeafAlbum() { + return true; + } +} diff --git a/src/com/android/gallery3d/data/MtpDeviceSet.java b/src/com/android/gallery3d/data/MtpDeviceSet.java new file mode 100644 index 000000000..6521623d4 --- /dev/null +++ b/src/com/android/gallery3d/data/MtpDeviceSet.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.util.MediaSetUtils; + +import android.mtp.MtpDeviceInfo; +import android.net.Uri; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +// MtpDeviceSet -- MtpDevice -- MtpImage +public class MtpDeviceSet extends MediaSet { + private static final String TAG = "MtpDeviceSet"; + + private GalleryApp mApplication; + private final ArrayList mDeviceSet = new ArrayList(); + private final ChangeNotifier mNotifier; + private final MtpContext mMtpContext; + private final String mName; + + public MtpDeviceSet(Path path, GalleryApp application, MtpContext mtpContext) { + super(path, nextVersionNumber()); + mApplication = application; + mNotifier = new ChangeNotifier(this, Uri.parse("mtp://"), application); + mMtpContext = mtpContext; + mName = application.getResources().getString(R.string.set_label_mtp_devices); + } + + private void loadDevices() { + DataManager dataManager = mApplication.getDataManager(); + // Enumerate all devices + mDeviceSet.clear(); + List devices = mMtpContext.getMtpClient().getDeviceList(); + Log.v(TAG, "loadDevices: " + devices + ", size=" + devices.size()); + for (android.mtp.MtpDevice mtpDevice : devices) { + int deviceId = mtpDevice.getDeviceId(); + Path childPath = mPath.getChild(deviceId); + MtpDevice device = (MtpDevice) dataManager.peekMediaObject(childPath); + if (device == null) { + device = new MtpDevice(childPath, mApplication, deviceId, mMtpContext); + } + Log.d(TAG, "add device " + device); + mDeviceSet.add(device); + } + + Collections.sort(mDeviceSet, MediaSetUtils.NAME_COMPARATOR); + for (int i = 0, n = mDeviceSet.size(); i < n; i++) { + mDeviceSet.get(i).reload(); + } + } + + public static String getDeviceName(MtpContext mtpContext, int deviceId) { + android.mtp.MtpDevice device = mtpContext.getMtpClient().getDevice(deviceId); + if (device == null) { + return ""; + } + MtpDeviceInfo info = device.getDeviceInfo(); + if (info == null) { + return ""; + } + String manufacturer = info.getManufacturer().trim(); + String model = info.getModel().trim(); + return manufacturer + " " + model; + } + + @Override + public MediaSet getSubMediaSet(int index) { + return index < mDeviceSet.size() ? mDeviceSet.get(index) : null; + } + + @Override + public int getSubMediaSetCount() { + return mDeviceSet.size(); + } + + @Override + public String getName() { + return mName; + } + + @Override + public long reload() { + if (mNotifier.isDirty()) { + mDataVersion = nextVersionNumber(); + loadDevices(); + } + return mDataVersion; + } +} diff --git a/src/com/android/gallery3d/data/MtpImage.java b/src/com/android/gallery3d/data/MtpImage.java new file mode 100644 index 000000000..4766d88f8 --- /dev/null +++ b/src/com/android/gallery3d/data/MtpImage.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.provider.GalleryProvider; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.graphics.Bitmap; +import android.graphics.BitmapRegionDecoder; +import android.hardware.usb.UsbDevice; +import android.mtp.MtpObjectInfo; +import android.net.Uri; +import android.util.Log; + +import java.text.DateFormat; +import java.util.Date; + +public class MtpImage extends MediaItem { + private static final String TAG = "MtpImage"; + + private final int mDeviceId; + private int mObjectId; + private int mObjectSize; + private long mDateTaken; + private String mFileName; + private final ThreadPool mThreadPool; + private final MtpContext mMtpContext; + private final MtpObjectInfo mObjInfo; + private final int mImageWidth; + private final int mImageHeight; + + MtpImage(Path path, GalleryApp application, int deviceId, + MtpObjectInfo objInfo, MtpContext mtpContext) { + super(path, nextVersionNumber()); + mDeviceId = deviceId; + mObjInfo = objInfo; + mObjectId = objInfo.getObjectHandle(); + mObjectSize = objInfo.getCompressedSize(); + mDateTaken = objInfo.getDateCreated(); + mFileName = objInfo.getName(); + mImageWidth = objInfo.getImagePixWidth(); + mImageHeight = objInfo.getImagePixHeight(); + mThreadPool = application.getThreadPool(); + mMtpContext = mtpContext; + } + + MtpImage(Path path, GalleryApp app, int deviceId, int objectId, MtpContext mtpContext) { + this(path, app, deviceId, MtpDevice.getObjectInfo(mtpContext, deviceId, objectId), + mtpContext); + } + + @Override + public long getDateInMs() { + return mDateTaken; + } + + @Override + public Job requestImage(int type) { + return new Job() { + public Bitmap run(JobContext jc) { + GetThumbnailBytes job = new GetThumbnailBytes(); + byte[] thumbnail = mThreadPool.submit(job).get(); + if (thumbnail == null) { + Log.w(TAG, "decoding thumbnail failed"); + return null; + } + return DecodeUtils.requestDecode(jc, thumbnail, null); + } + }; + } + + @Override + public Job requestLargeImage() { + return new Job() { + public BitmapRegionDecoder run(JobContext jc) { + byte[] bytes = mMtpContext.getMtpClient().getObject( + UsbDevice.getDeviceName(mDeviceId), mObjectId, mObjectSize); + return DecodeUtils.requestCreateBitmapRegionDecoder( + jc, bytes, 0, bytes.length, false); + } + }; + } + + public byte[] getImageData() { + return mMtpContext.getMtpClient().getObject( + UsbDevice.getDeviceName(mDeviceId), mObjectId, mObjectSize); + } + + @Override + public boolean Import() { + return mMtpContext.copyFile(UsbDevice.getDeviceName(mDeviceId), mObjInfo); + } + + @Override + public int getSupportedOperations() { + return SUPPORT_FULL_IMAGE | SUPPORT_IMPORT; + } + + private class GetThumbnailBytes implements Job { + public byte[] run(JobContext jc) { + return mMtpContext.getMtpClient().getThumbnail( + UsbDevice.getDeviceName(mDeviceId), mObjectId); + } + } + + public void updateContent(MtpObjectInfo info) { + if (mObjectId != info.getObjectHandle() || mDateTaken != info.getDateCreated()) { + mObjectId = info.getObjectHandle(); + mDateTaken = info.getDateCreated(); + mDataVersion = nextVersionNumber(); + } + } + + @Override + public String getMimeType() { + // Currently only JPEG is supported in MTP. + return "image/jpeg"; + } + + @Override + public int getMediaType() { + return MEDIA_TYPE_IMAGE; + } + + @Override + public long getSize() { + return mObjectSize; + } + + @Override + public Uri getContentUri() { + return GalleryProvider.BASE_URI.buildUpon() + .appendEncodedPath(mPath.toString().substring(1)) + .build(); + } + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + DateFormat formater = DateFormat.getDateTimeInstance(); + details.addDetail(MediaDetails.INDEX_TITLE, mFileName); + details.addDetail(MediaDetails.INDEX_DATETIME, formater.format(new Date(mDateTaken))); + details.addDetail(MediaDetails.INDEX_WIDTH, mImageWidth); + details.addDetail(MediaDetails.INDEX_HEIGHT, mImageHeight); + details.addDetail(MediaDetails.INDEX_SIZE, Long.valueOf(mObjectSize)); + return details; + } + +} diff --git a/src/com/android/gallery3d/data/MtpSource.java b/src/com/android/gallery3d/data/MtpSource.java new file mode 100644 index 000000000..683a40291 --- /dev/null +++ b/src/com/android/gallery3d/data/MtpSource.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.GalleryApp; + +class MtpSource extends MediaSource { + private static final String TAG = "MtpSource"; + + private static final int MTP_DEVICESET = 0; + private static final int MTP_DEVICE = 1; + private static final int MTP_ITEM = 2; + + GalleryApp mApplication; + PathMatcher mMatcher; + MtpContext mMtpContext; + + public MtpSource(GalleryApp application) { + super("mtp"); + mApplication = application; + mMatcher = new PathMatcher(); + mMatcher.add("/mtp", MTP_DEVICESET); + mMatcher.add("/mtp/*", MTP_DEVICE); + mMatcher.add("/mtp/item/*/*", MTP_ITEM); + mMtpContext = new MtpContext(mApplication.getAndroidContext()); + } + + @Override + public MediaObject createMediaObject(Path path) { + switch (mMatcher.match(path)) { + case MTP_DEVICESET: { + return new MtpDeviceSet(path, mApplication, mMtpContext); + } + case MTP_DEVICE: { + int deviceId = mMatcher.getIntVar(0); + return new MtpDevice(path, mApplication, deviceId, mMtpContext); + } + case MTP_ITEM: { + int deviceId = mMatcher.getIntVar(0); + int objectId = mMatcher.getIntVar(1); + return new MtpImage(path, mApplication, deviceId, objectId, mMtpContext); + } + default: + throw new RuntimeException("bad path: " + path); + } + } + + @Override + public void pause() { + mMtpContext.pause(); + } + + @Override + public void resume() { + mMtpContext.resume(); + } +} diff --git a/src/com/android/gallery3d/data/Path.java b/src/com/android/gallery3d/data/Path.java new file mode 100644 index 000000000..3de1c7c76 --- /dev/null +++ b/src/com/android/gallery3d/data/Path.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.IdentityCache; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +public class Path { + private static final String TAG = "Path"; + private static Path sRoot = new Path(null, "ROOT"); + + private final Path mParent; + private final String mSegment; + private WeakReference mObject; + private IdentityCache mChildren; + + private Path(Path parent, String segment) { + mParent = parent; + mSegment = segment; + } + + public Path getChild(String segment) { + synchronized (Path.class) { + if (mChildren == null) { + mChildren = new IdentityCache(); + } else { + Path p = mChildren.get(segment); + if (p != null) return p; + } + + Path p = new Path(this, segment); + mChildren.put(segment, p); + return p; + } + } + + public Path getParent() { + synchronized (Path.class) { + return mParent; + } + } + + public Path getChild(int segment) { + return getChild(String.valueOf(segment)); + } + + public Path getChild(long segment) { + return getChild(String.valueOf(segment)); + } + + public void setObject(MediaObject object) { + synchronized (Path.class) { + Utils.assertTrue(mObject == null || mObject.get() == null); + mObject = new WeakReference(object); + } + } + + public MediaObject getObject() { + synchronized (Path.class) { + return (mObject == null) ? null : mObject.get(); + } + } + + @Override + public String toString() { + synchronized (Path.class) { + StringBuilder sb = new StringBuilder(); + String[] segments = split(); + for (int i = 0; i < segments.length; i++) { + sb.append("/"); + sb.append(segments[i]); + } + return sb.toString(); + } + } + + public static Path fromString(String s) { + synchronized (Path.class) { + String[] segments = split(s); + Path current = sRoot; + for (int i = 0; i < segments.length; i++) { + current = current.getChild(segments[i]); + } + return current; + } + } + + public String[] split() { + synchronized (Path.class) { + int n = 0; + for (Path p = this; p != sRoot; p = p.mParent) { + n++; + } + String[] segments = new String[n]; + int i = n - 1; + for (Path p = this; p != sRoot; p = p.mParent) { + segments[i--] = p.mSegment; + } + return segments; + } + } + + public static String[] split(String s) { + int n = s.length(); + if (n == 0) return new String[0]; + if (s.charAt(0) != '/') { + throw new RuntimeException("malformed path:" + s); + } + ArrayList segments = new ArrayList(); + int i = 1; + while (i < n) { + int brace = 0; + int j; + for (j = i; j < n; j++) { + char c = s.charAt(j); + if (c == '{') ++brace; + else if (c == '}') --brace; + else if (brace == 0 && c == '/') break; + } + if (brace != 0) { + throw new RuntimeException("unbalanced brace in path:" + s); + } + segments.add(s.substring(i, j)); + i = j + 1; + } + String[] result = new String[segments.size()]; + segments.toArray(result); + return result; + } + + // Splits a string to an array of strings. + // For example, "{foo,bar,baz}" -> {"foo","bar","baz"}. + public static String[] splitSequence(String s) { + int n = s.length(); + if (s.charAt(0) != '{' || s.charAt(n-1) != '}') { + throw new RuntimeException("bad sequence: " + s); + } + ArrayList segments = new ArrayList(); + int i = 1; + while (i < n - 1) { + int brace = 0; + int j; + for (j = i; j < n - 1; j++) { + char c = s.charAt(j); + if (c == '{') ++brace; + else if (c == '}') --brace; + else if (brace == 0 && c == ',') break; + } + if (brace != 0) { + throw new RuntimeException("unbalanced brace in path:" + s); + } + segments.add(s.substring(i, j)); + i = j + 1; + } + String[] result = new String[segments.size()]; + segments.toArray(result); + return result; + } + + public String getPrefix() { + synchronized (Path.class) { + Path current = this; + if (current == sRoot) return ""; + while (current.mParent != sRoot) { + current = current.mParent; + } + return current.mSegment; + } + } + + public String getSuffix() { + // We don't need lock because mSegment is final. + return mSegment; + } + + public String getSuffix(int level) { + // We don't need lock because mSegment and mParent are final. + Path p = this; + while (level-- != 0) { + p = p.mParent; + } + return p.mSegment; + } + + // Below are for testing/debugging only + static void clearAll() { + synchronized (Path.class) { + sRoot = new Path(null, ""); + } + } + + static void dumpAll() { + dumpAll(sRoot, "", ""); + } + + static void dumpAll(Path p, String prefix1, String prefix2) { + synchronized (Path.class) { + MediaObject obj = p.getObject(); + Log.d(TAG, prefix1 + p.mSegment + ":" + + (obj == null ? "null" : obj.getClass().getSimpleName())); + if (p.mChildren != null) { + ArrayList childrenKeys = p.mChildren.keys(); + int i = 0, n = childrenKeys.size(); + for (String key : childrenKeys) { + Path child = p.mChildren.get(key); + if (child == null) { + ++i; + continue; + } + Log.d(TAG, prefix2 + "|"); + if (++i < n) { + dumpAll(child, prefix2 + "+-- ", prefix2 + "| "); + } else { + dumpAll(child, prefix2 + "+-- ", prefix2 + " "); + } + } + } + } + } +} diff --git a/src/com/android/gallery3d/data/PathMatcher.java b/src/com/android/gallery3d/data/PathMatcher.java new file mode 100644 index 000000000..9c6b840d5 --- /dev/null +++ b/src/com/android/gallery3d/data/PathMatcher.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import java.util.ArrayList; +import java.util.HashMap; + +public class PathMatcher { + public static final int NOT_FOUND = -1; + + private ArrayList mVariables = new ArrayList(); + private Node mRoot = new Node(); + + public PathMatcher() { + mRoot = new Node(); + } + + public void add(String pattern, int kind) { + String[] segments = Path.split(pattern); + Node current = mRoot; + for (int i = 0; i < segments.length; i++) { + current = current.addChild(segments[i]); + } + current.setKind(kind); + } + + public int match(Path path) { + String[] segments = path.split(); + mVariables.clear(); + Node current = mRoot; + for (int i = 0; i < segments.length; i++) { + Node next = current.getChild(segments[i]); + if (next == null) { + next = current.getChild("*"); + if (next != null) { + mVariables.add(segments[i]); + } else { + return NOT_FOUND; + } + } + current = next; + } + return current.getKind(); + } + + public String getVar(int index) { + return mVariables.get(index); + } + + public int getIntVar(int index) { + return Integer.parseInt(mVariables.get(index)); + } + + public long getLongVar(int index) { + return Long.parseLong(mVariables.get(index)); + } + + private static class Node { + private HashMap mMap; + private int mKind = NOT_FOUND; + + Node addChild(String segment) { + if (mMap == null) { + mMap = new HashMap(); + } else { + Node node = mMap.get(segment); + if (node != null) return node; + } + + Node n = new Node(); + mMap.put(segment, n); + return n; + } + + Node getChild(String segment) { + if (mMap == null) return null; + return mMap.get(segment); + } + + void setKind(int kind) { + mKind = kind; + } + + int getKind() { + return mKind; + } + } +} diff --git a/src/com/android/gallery3d/data/SizeClustering.java b/src/com/android/gallery3d/data/SizeClustering.java new file mode 100644 index 000000000..7e24b337b --- /dev/null +++ b/src/com/android/gallery3d/data/SizeClustering.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.R; + +import android.content.Context; +import android.content.res.Resources; + +import java.util.ArrayList; + +public class SizeClustering extends Clustering { + private static final String TAG = "SizeClustering"; + + private Context mContext; + private ArrayList[] mClusters; + private String[] mNames; + private long mMinSizes[]; + + private static final long MEGA_BYTES = 1024L*1024; + private static final long GIGA_BYTES = 1024L*1024*1024; + + private static final long[] SIZE_LEVELS = { + 0, + 1 * MEGA_BYTES, + 10 * MEGA_BYTES, + 100 * MEGA_BYTES, + 1 * GIGA_BYTES, + 2 * GIGA_BYTES, + 4 * GIGA_BYTES, + }; + + public SizeClustering(Context context) { + mContext = context; + } + + @Override + public void run(MediaSet baseSet) { + final ArrayList[] group = + (ArrayList[]) new ArrayList[SIZE_LEVELS.length]; + baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + public void consume(int index, MediaItem item) { + // Find the cluster this item belongs to. + long size = item.getSize(); + int i; + for (i = 0; i < SIZE_LEVELS.length - 1; i++) { + if (size < SIZE_LEVELS[i + 1]) { + break; + } + } + + ArrayList list = group[i]; + if (list == null) { + list = new ArrayList(); + group[i] = list; + } + list.add(item.getPath()); + } + }); + + int count = 0; + for (int i = 0; i < group.length; i++) { + if (group[i] != null) { + count++; + } + } + + mClusters = (ArrayList[]) new ArrayList[count]; + mNames = new String[count]; + mMinSizes = new long[count]; + + Resources res = mContext.getResources(); + int k = 0; + // Go through group in the reverse order, so the group with the largest + // size will show first. + for (int i = group.length - 1; i >= 0; i--) { + if (group[i] == null) continue; + + mClusters[k] = group[i]; + if (i == 0) { + mNames[k] = String.format( + res.getString(R.string.size_below), getSizeString(i + 1)); + } else if (i == group.length - 1) { + mNames[k] = String.format( + res.getString(R.string.size_above), getSizeString(i)); + } else { + String minSize = getSizeString(i); + String maxSize = getSizeString(i + 1); + mNames[k] = String.format( + res.getString(R.string.size_between), minSize, maxSize); + } + mMinSizes[k] = SIZE_LEVELS[i]; + k++; + } + } + + private String getSizeString(int index) { + long bytes = SIZE_LEVELS[index]; + if (bytes >= GIGA_BYTES) { + return (bytes / GIGA_BYTES) + "GB"; + } else { + return (bytes / MEGA_BYTES) + "MB"; + } + } + + @Override + public int getNumberOfClusters() { + return mClusters.length; + } + + @Override + public ArrayList getCluster(int index) { + return mClusters[index]; + } + + @Override + public String getClusterName(int index) { + return mNames[index]; + } + + public long getMinSize(int index) { + return mMinSizes[index]; + } +} diff --git a/src/com/android/gallery3d/data/TagClustering.java b/src/com/android/gallery3d/data/TagClustering.java new file mode 100644 index 000000000..c87305132 --- /dev/null +++ b/src/com/android/gallery3d/data/TagClustering.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.R; + +import android.content.Context; + +import java.util.ArrayList; +import java.util.Map; +import java.util.TreeMap; + +public class TagClustering extends Clustering { + @SuppressWarnings("unused") + private static final String TAG = "TagClustering"; + + private ArrayList> mClusters; + private String[] mNames; + private String mUntaggedString; + + public TagClustering(Context context) { + mUntaggedString = context.getResources().getString(R.string.untagged); + } + + @Override + public void run(MediaSet baseSet) { + final TreeMap> map = + new TreeMap>(); + final ArrayList untagged = new ArrayList(); + + baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + public void consume(int index, MediaItem item) { + Path path = item.getPath(); + + String[] tags = item.getTags(); + if (tags == null || tags.length == 0) { + untagged.add(path); + return; + } + for (int j = 0; j < tags.length; j++) { + String key = tags[j]; + ArrayList list = map.get(key); + if (list == null) { + list = new ArrayList(); + map.put(key, list); + } + list.add(path); + } + } + }); + + int m = map.size(); + mClusters = new ArrayList>(); + mNames = new String[m + ((untagged.size() > 0) ? 1 : 0)]; + int i = 0; + for (Map.Entry> entry : map.entrySet()) { + mNames[i++] = entry.getKey(); + mClusters.add(entry.getValue()); + } + if (untagged.size() > 0) { + mNames[i++] = mUntaggedString; + mClusters.add(untagged); + } + } + + @Override + public int getNumberOfClusters() { + return mClusters.size(); + } + + @Override + public ArrayList getCluster(int index) { + return mClusters.get(index); + } + + @Override + public String getClusterName(int index) { + return mNames[index]; + } +} diff --git a/src/com/android/gallery3d/data/TimeClustering.java b/src/com/android/gallery3d/data/TimeClustering.java new file mode 100644 index 000000000..1ccf14c13 --- /dev/null +++ b/src/com/android/gallery3d/data/TimeClustering.java @@ -0,0 +1,436 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.Context; +import android.text.format.DateFormat; +import android.text.format.DateUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +public class TimeClustering extends Clustering { + private static final String TAG = "TimeClustering"; + + // If 2 items are greater than 25 miles apart, they will be in different + // clusters. + private static final int GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES = 20; + + // Do not want to split based on anything under 1 min. + private static final long MIN_CLUSTER_SPLIT_TIME_IN_MS = 60000L; + + // Disregard a cluster split time of anything over 2 hours. + private static final long MAX_CLUSTER_SPLIT_TIME_IN_MS = 7200000L; + + // Try and get around 9 clusters (best-effort for the common case). + private static final int NUM_CLUSTERS_TARGETED = 9; + + // Try and merge 2 clusters if they are both smaller than min cluster size. + // The min cluster size can range from 8 to 15. + private static final int MIN_MIN_CLUSTER_SIZE = 8; + private static final int MAX_MIN_CLUSTER_SIZE = 15; + + // Try and split a cluster if it is bigger than max cluster size. + // The max cluster size can range from 20 to 50. + private static final int MIN_MAX_CLUSTER_SIZE = 20; + private static final int MAX_MAX_CLUSTER_SIZE = 50; + + // Initially put 2 items in the same cluster as long as they are within + // 3 cluster frequencies of each other. + private static int CLUSTER_SPLIT_MULTIPLIER = 3; + + // The minimum change factor in the time between items to consider a + // partition. + // Example: (Item 3 - Item 2) / (Item 2 - Item 1). + private static final int MIN_PARTITION_CHANGE_FACTOR = 2; + + // Make the cluster split time of a large cluster half that of a regular + // cluster. + private static final int PARTITION_CLUSTER_SPLIT_TIME_FACTOR = 2; + + private Context mContext; + private ArrayList mClusters; + private String[] mNames; + private Cluster mCurrCluster; + + private long mClusterSplitTime = + (MIN_CLUSTER_SPLIT_TIME_IN_MS + MAX_CLUSTER_SPLIT_TIME_IN_MS) / 2; + private long mLargeClusterSplitTime = + mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR; + private int mMinClusterSize = (MIN_MIN_CLUSTER_SIZE + MAX_MIN_CLUSTER_SIZE) / 2; + private int mMaxClusterSize = (MIN_MAX_CLUSTER_SIZE + MAX_MAX_CLUSTER_SIZE) / 2; + + + private static final Comparator sDateComparator = + new DateComparator(); + + private static class DateComparator implements Comparator { + public int compare(SmallItem item1, SmallItem item2) { + return -Utils.compare(item1.dateInMs, item2.dateInMs); + } + } + + public TimeClustering(Context context) { + mContext = context; + mClusters = new ArrayList(); + mCurrCluster = new Cluster(); + } + + @Override + public void run(MediaSet baseSet) { + final int total = baseSet.getTotalMediaItemCount(); + final SmallItem[] buf = new SmallItem[total]; + final double[] latLng = new double[2]; + + baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + public void consume(int index, MediaItem item) { + if (index < 0 || index >= total) return; + SmallItem s = new SmallItem(); + s.path = item.getPath(); + s.dateInMs = item.getDateInMs(); + item.getLatLong(latLng); + s.lat = latLng[0]; + s.lng = latLng[1]; + buf[index] = s; + } + }); + + ArrayList items = new ArrayList(total); + for (int i = 0; i < total; i++) { + if (buf[i] != null) { + items.add(buf[i]); + } + } + + Collections.sort(items, sDateComparator); + + int n = items.size(); + long minTime = 0; + long maxTime = 0; + for (int i = 0; i < n; i++) { + long t = items.get(i).dateInMs; + if (t == 0) continue; + if (minTime == 0) { + minTime = maxTime = t; + } else { + minTime = Math.min(minTime, t); + maxTime = Math.max(maxTime, t); + } + } + + setTimeRange(maxTime - minTime, n); + + for (int i = 0; i < n; i++) { + compute(items.get(i)); + } + + compute(null); + + int m = mClusters.size(); + mNames = new String[m]; + for (int i = 0; i < m; i++) { + mNames[i] = mClusters.get(i).generateCaption(mContext); + } + } + + @Override + public int getNumberOfClusters() { + return mClusters.size(); + } + + @Override + public ArrayList getCluster(int index) { + ArrayList items = mClusters.get(index).getItems(); + ArrayList result = new ArrayList(items.size()); + for (int i = 0, n = items.size(); i < n; i++) { + result.add(items.get(i).path); + } + return result; + } + + @Override + public String getClusterName(int index) { + return mNames[index]; + } + + private void setTimeRange(long timeRange, int numItems) { + if (numItems != 0) { + int meanItemsPerCluster = numItems / NUM_CLUSTERS_TARGETED; + // Heuristic to get min and max cluster size - half and double the + // desired items per cluster. + mMinClusterSize = meanItemsPerCluster / 2; + mMaxClusterSize = meanItemsPerCluster * 2; + mClusterSplitTime = timeRange / numItems * CLUSTER_SPLIT_MULTIPLIER; + } + mClusterSplitTime = Utils.clamp(mClusterSplitTime, MIN_CLUSTER_SPLIT_TIME_IN_MS, MAX_CLUSTER_SPLIT_TIME_IN_MS); + mLargeClusterSplitTime = mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR; + mMinClusterSize = Utils.clamp(mMinClusterSize, MIN_MIN_CLUSTER_SIZE, MAX_MIN_CLUSTER_SIZE); + mMaxClusterSize = Utils.clamp(mMaxClusterSize, MIN_MAX_CLUSTER_SIZE, MAX_MAX_CLUSTER_SIZE); + } + + private void compute(SmallItem currentItem) { + if (currentItem != null) { + int numClusters = mClusters.size(); + int numCurrClusterItems = mCurrCluster.size(); + boolean geographicallySeparateItem = false; + boolean itemAddedToCurrentCluster = false; + + // Determine if this item should go in the current cluster or be the + // start of a new cluster. + if (numCurrClusterItems == 0) { + mCurrCluster.addItem(currentItem); + } else { + SmallItem prevItem = mCurrCluster.getLastItem(); + if (isGeographicallySeparated(prevItem, currentItem)) { + mClusters.add(mCurrCluster); + geographicallySeparateItem = true; + } else if (numCurrClusterItems > mMaxClusterSize) { + splitAndAddCurrentCluster(); + } else if (timeDistance(prevItem, currentItem) < mClusterSplitTime) { + mCurrCluster.addItem(currentItem); + itemAddedToCurrentCluster = true; + } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize + && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) { + mergeAndAddCurrentCluster(); + } else { + mClusters.add(mCurrCluster); + } + + // Creating a new cluster and adding the current item to it. + if (!itemAddedToCurrentCluster) { + mCurrCluster = new Cluster(); + if (geographicallySeparateItem) { + mCurrCluster.mGeographicallySeparatedFromPrevCluster = true; + } + mCurrCluster.addItem(currentItem); + } + } + } else { + if (mCurrCluster.size() > 0) { + int numClusters = mClusters.size(); + int numCurrClusterItems = mCurrCluster.size(); + + // The last cluster may potentially be too big or too small. + if (numCurrClusterItems > mMaxClusterSize) { + splitAndAddCurrentCluster(); + } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize + && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) { + mergeAndAddCurrentCluster(); + } else { + mClusters.add(mCurrCluster); + } + mCurrCluster = new Cluster(); + } + } + } + + private void splitAndAddCurrentCluster() { + ArrayList currClusterItems = mCurrCluster.getItems(); + int numCurrClusterItems = mCurrCluster.size(); + int secondPartitionStartIndex = getPartitionIndexForCurrentCluster(); + if (secondPartitionStartIndex != -1) { + Cluster partitionedCluster = new Cluster(); + for (int j = 0; j < secondPartitionStartIndex; j++) { + partitionedCluster.addItem(currClusterItems.get(j)); + } + mClusters.add(partitionedCluster); + partitionedCluster = new Cluster(); + for (int j = secondPartitionStartIndex; j < numCurrClusterItems; j++) { + partitionedCluster.addItem(currClusterItems.get(j)); + } + mClusters.add(partitionedCluster); + } else { + mClusters.add(mCurrCluster); + } + } + + private int getPartitionIndexForCurrentCluster() { + int partitionIndex = -1; + float largestChange = MIN_PARTITION_CHANGE_FACTOR; + ArrayList currClusterItems = mCurrCluster.getItems(); + int numCurrClusterItems = mCurrCluster.size(); + int minClusterSize = mMinClusterSize; + + // Could be slightly more efficient here but this code seems cleaner. + if (numCurrClusterItems > minClusterSize + 1) { + for (int i = minClusterSize; i < numCurrClusterItems - minClusterSize; i++) { + SmallItem prevItem = currClusterItems.get(i - 1); + SmallItem currItem = currClusterItems.get(i); + SmallItem nextItem = currClusterItems.get(i + 1); + + long timeNext = nextItem.dateInMs; + long timeCurr = currItem.dateInMs; + long timePrev = prevItem.dateInMs; + + if (timeNext == 0 || timeCurr == 0 || timePrev == 0) continue; + + long diff1 = Math.abs(timeNext - timeCurr); + long diff2 = Math.abs(timeCurr - timePrev); + + float change = Math.max(diff1 / (diff2 + 0.01f), diff2 / (diff1 + 0.01f)); + if (change > largestChange) { + if (timeDistance(currItem, prevItem) > mLargeClusterSplitTime) { + partitionIndex = i; + largestChange = change; + } else if (timeDistance(nextItem, currItem) > mLargeClusterSplitTime) { + partitionIndex = i + 1; + largestChange = change; + } + } + } + } + return partitionIndex; + } + + private void mergeAndAddCurrentCluster() { + int numClusters = mClusters.size(); + Cluster prevCluster = mClusters.get(numClusters - 1); + ArrayList currClusterItems = mCurrCluster.getItems(); + int numCurrClusterItems = mCurrCluster.size(); + if (prevCluster.size() < mMinClusterSize) { + for (int i = 0; i < numCurrClusterItems; i++) { + prevCluster.addItem(currClusterItems.get(i)); + } + mClusters.set(numClusters - 1, prevCluster); + } else { + mClusters.add(mCurrCluster); + } + } + + // Returns true if a, b are sufficiently geographically separated. + private static boolean isGeographicallySeparated(SmallItem itemA, SmallItem itemB) { + if (!GalleryUtils.isValidLocation(itemA.lat, itemA.lng) + || !GalleryUtils.isValidLocation(itemB.lat, itemB.lng)) { + return false; + } + + double distance = GalleryUtils.fastDistanceMeters( + Math.toRadians(itemA.lat), + Math.toRadians(itemA.lng), + Math.toRadians(itemB.lat), + Math.toRadians(itemB.lng)); + return (GalleryUtils.toMile(distance) > GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES); + } + + // Returns the time interval between the two items in milliseconds. + private static long timeDistance(SmallItem a, SmallItem b) { + return Math.abs(a.dateInMs - b.dateInMs); + } +} + +class SmallItem { + Path path; + long dateInMs; + double lat, lng; +} + +class Cluster { + @SuppressWarnings("unused") + private static final String TAG = "Cluster"; + private static final String MMDDYY_FORMAT = "MMddyy"; + + // This is for TimeClustering only. + public boolean mGeographicallySeparatedFromPrevCluster = false; + + private ArrayList mItems = new ArrayList(); + + public Cluster() { + } + + public void addItem(SmallItem item) { + mItems.add(item); + } + + public int size() { + return mItems.size(); + } + + public SmallItem getLastItem() { + int n = mItems.size(); + return (n == 0) ? null : mItems.get(n - 1); + } + + public ArrayList getItems() { + return mItems; + } + + public String generateCaption(Context context) { + int n = mItems.size(); + long minTimestamp = 0; + long maxTimestamp = 0; + + for (int i = 0; i < n; i++) { + long t = mItems.get(i).dateInMs; + if (t == 0) continue; + if (minTimestamp == 0) { + minTimestamp = maxTimestamp = t; + } else { + minTimestamp = Math.min(minTimestamp, t); + maxTimestamp = Math.max(maxTimestamp, t); + } + } + if (minTimestamp == 0) return ""; + + String caption; + String minDay = DateFormat.format(MMDDYY_FORMAT, minTimestamp) + .toString(); + String maxDay = DateFormat.format(MMDDYY_FORMAT, maxTimestamp) + .toString(); + + if (minDay.substring(4).equals(maxDay.substring(4))) { + // The items are from the same year - show at least as + // much granularity as abbrev_all allows. + caption = DateUtils.formatDateRange(context, minTimestamp, + maxTimestamp, DateUtils.FORMAT_ABBREV_ALL); + + // Get a more granular date range string if the min and + // max timestamp are on the same day and from the + // current year. + if (minDay.equals(maxDay)) { + int flags = DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE; + // Contains the year only if the date does not + // correspond to the current year. + String dateRangeWithOptionalYear = DateUtils.formatDateTime( + context, minTimestamp, flags); + String dateRangeWithYear = DateUtils.formatDateTime( + context, minTimestamp, flags | DateUtils.FORMAT_SHOW_YEAR); + if (!dateRangeWithOptionalYear.equals(dateRangeWithYear)) { + // This means both dates are from the same year + // - show the time. + // Not enough room to display the time range. + // Pick the mid-point. + long midTimestamp = (minTimestamp + maxTimestamp) / 2; + caption = DateUtils.formatDateRange(context, midTimestamp, + midTimestamp, DateUtils.FORMAT_SHOW_TIME | flags); + } + } + } else { + // The items are not from the same year - only show + // month and year. + int flags = DateUtils.FORMAT_NO_MONTH_DAY + | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE; + caption = DateUtils.formatDateRange(context, minTimestamp, + maxTimestamp, flags); + } + + return caption; + } +} diff --git a/src/com/android/gallery3d/data/UriImage.java b/src/com/android/gallery3d/data/UriImage.java new file mode 100644 index 000000000..3a7ed7c3f --- /dev/null +++ b/src/com/android/gallery3d/data/UriImage.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.content.ContentResolver; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory.Options; +import android.graphics.BitmapRegionDecoder; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.webkit.MimeTypeMap; + +import java.io.FileNotFoundException; +import java.net.URI; +import java.net.URL; + +public class UriImage extends MediaItem { + private static final String TAG = "UriImage"; + + private static final int STATE_INIT = 0; + private static final int STATE_DOWNLOADING = 1; + private static final int STATE_DOWNLOADED = 2; + private static final int STATE_ERROR = -1; + + private final Uri mUri; + private final String mContentType; + + private DownloadCache.Entry mCacheEntry; + private ParcelFileDescriptor mFileDescriptor; + private int mState = STATE_INIT; + private int mWidth; + private int mHeight; + + private GalleryApp mApplication; + + public UriImage(GalleryApp application, Path path, Uri uri) { + super(path, nextVersionNumber()); + mUri = uri; + mApplication = Utils.checkNotNull(application); + mContentType = getMimeType(uri); + } + + private String getMimeType(Uri uri) { + if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { + String extension = + MimeTypeMap.getFileExtensionFromUrl(uri.toString()); + String type = MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(extension); + if (type != null) return type; + } + return mApplication.getContentResolver().getType(uri); + } + + @Override + public Job requestImage(int type) { + return new BitmapJob(type); + } + + @Override + public Job requestLargeImage() { + return new RegionDecoderJob(); + } + + private void openFileOrDownloadTempFile(JobContext jc) { + int state = openOrDownloadInner(jc); + synchronized (this) { + mState = state; + if (mState != STATE_DOWNLOADED) { + if (mFileDescriptor != null) { + Utils.closeSilently(mFileDescriptor); + mFileDescriptor = null; + } + } + notifyAll(); + } + } + + private int openOrDownloadInner(JobContext jc) { + String scheme = mUri.getScheme(); + if (ContentResolver.SCHEME_CONTENT.equals(scheme) + || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme) + || ContentResolver.SCHEME_FILE.equals(scheme)) { + try { + mFileDescriptor = mApplication.getContentResolver() + .openFileDescriptor(mUri, "r"); + if (jc.isCancelled()) return STATE_INIT; + return STATE_DOWNLOADED; + } catch (FileNotFoundException e) { + Log.w(TAG, "fail to open: " + mUri, e); + return STATE_ERROR; + } + } else { + try { + URL url = new URI(mUri.toString()).toURL(); + mCacheEntry = mApplication.getDownloadCache().download(jc, url); + if (jc.isCancelled()) return STATE_INIT; + if (mCacheEntry == null) { + Log.w(TAG, "download failed " + url); + return STATE_ERROR; + } + mFileDescriptor = ParcelFileDescriptor.open( + mCacheEntry.cacheFile, ParcelFileDescriptor.MODE_READ_ONLY); + return STATE_DOWNLOADED; + } catch (Throwable t) { + Log.w(TAG, "download error", t); + return STATE_ERROR; + } + } + } + + private boolean prepareInputFile(JobContext jc) { + jc.setCancelListener(new CancelListener() { + public void onCancel() { + synchronized (this) { + notifyAll(); + } + } + }); + + while (true) { + synchronized (this) { + if (jc.isCancelled()) return false; + if (mState == STATE_INIT) { + mState = STATE_DOWNLOADING; + // Then leave the synchronized block and continue. + } else if (mState == STATE_ERROR) { + return false; + } else if (mState == STATE_DOWNLOADED) { + return true; + } else /* if (mState == STATE_DOWNLOADING) */ { + try { + wait(); + } catch (InterruptedException ex) { + // ignored. + } + continue; + } + } + // This is only reached for STATE_INIT->STATE_DOWNLOADING + openFileOrDownloadTempFile(jc); + } + } + + private class RegionDecoderJob implements Job { + public BitmapRegionDecoder run(JobContext jc) { + if (!prepareInputFile(jc)) return null; + BitmapRegionDecoder decoder = DecodeUtils.requestCreateBitmapRegionDecoder( + jc, mFileDescriptor.getFileDescriptor(), false); + mWidth = decoder.getWidth(); + mHeight = decoder.getHeight(); + return decoder; + } + } + + private class BitmapJob implements Job { + private int mType; + + protected BitmapJob(int type) { + mType = type; + } + + public Bitmap run(JobContext jc) { + if (!prepareInputFile(jc)) return null; + int targetSize = LocalImage.getTargetSize(mType); + Options options = new Options(); + options.inPreferredConfig = Config.ARGB_8888; + Bitmap bitmap = DecodeUtils.requestDecode(jc, + mFileDescriptor.getFileDescriptor(), options, targetSize); + if (jc.isCancelled() || bitmap == null) { + return null; + } + + if (mType == MediaItem.TYPE_MICROTHUMBNAIL) { + bitmap = BitmapUtils.resizeDownAndCropCenter(bitmap, + targetSize, true); + } else { + bitmap = BitmapUtils.resizeDownBySideLength(bitmap, + targetSize, true); + } + + return bitmap; + } + } + + @Override + public int getSupportedOperations() { + int supported = SUPPORT_EDIT | SUPPORT_SETAS; + if (isSharable()) supported |= SUPPORT_SHARE; + if (BitmapUtils.isSupportedByRegionDecoder(mContentType)) { + supported |= SUPPORT_FULL_IMAGE; + } + return supported; + } + + private boolean isSharable() { + // We cannot grant read permission to the receiver since we put + // the data URI in EXTRA_STREAM instead of the data part of an intent + // And there are issues in MediaUploader and Bluetooth file sender to + // share a general image data. So, we only share for local file. + return ContentResolver.SCHEME_FILE.equals(mUri.getScheme()); + } + + @Override + public int getMediaType() { + return MEDIA_TYPE_IMAGE; + } + + @Override + public Uri getContentUri() { + return mUri; + } + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + if (mWidth != 0 && mHeight != 0) { + details.addDetail(MediaDetails.INDEX_WIDTH, mWidth); + details.addDetail(MediaDetails.INDEX_HEIGHT, mHeight); + } + details.addDetail(MediaDetails.INDEX_MIMETYPE, mContentType); + if (ContentResolver.SCHEME_FILE.equals(mUri.getScheme())) { + String filePath = mUri.getPath(); + details.addDetail(MediaDetails.INDEX_PATH, filePath); + MediaDetails.extractExifInfo(details, filePath); + } + return details; + } + + @Override + public String getMimeType() { + return mContentType; + } + + @Override + protected void finalize() throws Throwable { + try { + if (mFileDescriptor != null) { + Utils.closeSilently(mFileDescriptor); + } + } finally { + super.finalize(); + } + } +} diff --git a/src/com/android/gallery3d/data/UriSource.java b/src/com/android/gallery3d/data/UriSource.java new file mode 100644 index 000000000..ac62b93a7 --- /dev/null +++ b/src/com/android/gallery3d/data/UriSource.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.app.GalleryApp; + +import android.net.Uri; + +import java.net.URLDecoder; +import java.net.URLEncoder; + +class UriSource extends MediaSource { + @SuppressWarnings("unused") + private static final String TAG = "UriSource"; + + private GalleryApp mApplication; + + public UriSource(GalleryApp context) { + super("uri"); + mApplication = context; + } + + @Override + public MediaObject createMediaObject(Path path) { + String segment[] = path.split(); + if (segment.length != 2) { + throw new RuntimeException("bad path: " + path); + } + + String decoded = URLDecoder.decode(segment[1]); + return new UriImage(mApplication, path, Uri.parse(decoded)); + } + + @Override + public Path findPathByUri(Uri uri) { + String type = mApplication.getContentResolver().getType(uri); + // Assume the type is image if the type cannot be resolved + // This could happen for "http" URI. + if (type == null || type.startsWith("image/")) { + return Path.fromString("/uri/" + URLEncoder.encode(uri.toString())); + } + return null; + } +} diff --git a/src/com/android/gallery3d/provider/GalleryProvider.java b/src/com/android/gallery3d/provider/GalleryProvider.java new file mode 100644 index 000000000..f5f0f1b3c --- /dev/null +++ b/src/com/android/gallery3d/provider/GalleryProvider.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.provider; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.DownloadCache; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MtpImage; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.picasasource.PicasaSource; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Binder; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.provider.MediaStore.Images.ImageColumns; +import android.util.Log; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; + +public class GalleryProvider extends ContentProvider { + private static final String TAG = "GalleryProvider"; + + public static final String AUTHORITY = "com.android.gallery3d.provider"; + public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY); + + private DataManager mDataManager; + private DownloadCache mDownloadCache; + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + // TODO: consider concurrent access + @Override + public String getType(Uri uri) { + long token = Binder.clearCallingIdentity(); + try { + Path path = Path.fromString(uri.getPath()); + MediaItem item = (MediaItem) mDataManager.getMediaObject(path); + return item != null ? item.getMimeType() : null; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean onCreate() { + GalleryApp app = (GalleryApp) getContext().getApplicationContext(); + mDataManager = app.getDataManager(); + return true; + } + + private DownloadCache getDownloadCache() { + if (mDownloadCache == null) { + GalleryApp app = (GalleryApp) getContext().getApplicationContext(); + mDownloadCache = app.getDownloadCache(); + } + return mDownloadCache; + } + + // TODO: consider concurrent access + @Override + public Cursor query(Uri uri, String[] projection, + String selection, String[] selectionArgs, String sortOrder) { + long token = Binder.clearCallingIdentity(); + try { + Path path = Path.fromString(uri.getPath()); + MediaObject object = mDataManager.getMediaObject(path); + if (object == null) { + Log.w(TAG, "cannot find: " + uri); + return null; + } + if (PicasaSource.isPicasaImage(object)) { + return queryPicasaItem(object, + projection, selection, selectionArgs, sortOrder); + } else if (object instanceof MtpImage) { + return queryMtpItem((MtpImage) object, + projection, selection, selectionArgs, sortOrder); + } else { + return null; + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private Cursor queryMtpItem(MtpImage image, String[] projection, + String selection, String[] selectionArgs, String sortOrder) { + Object[] columnValues = new Object[projection.length]; + for (int i = 0, n = projection.length; i < n; ++i) { + String column = projection[i]; + if (ImageColumns.DISPLAY_NAME.equals(column)) { + columnValues[i] = image.getName(); + } else if (ImageColumns.SIZE.equals(column)){ + columnValues[i] = image.getSize(); + } else if (ImageColumns.MIME_TYPE.equals(column)) { + columnValues[i] = image.getMimeType(); + } else if (ImageColumns.DATE_TAKEN.equals(column)) { + columnValues[i] = image.getDateInMs(); + } else { + Log.w(TAG, "unsupported column: " + column); + } + } + MatrixCursor cursor = new MatrixCursor(projection); + cursor.addRow(columnValues); + return cursor; + } + + private Cursor queryPicasaItem(MediaObject image, String[] projection, + String selection, String[] selectionArgs, String sortOrder) { + Object[] columnValues = new Object[projection.length]; + double latitude = PicasaSource.getLatitude(image); + double longitude = PicasaSource.getLongitude(image); + boolean isValidLatlong = GalleryUtils.isValidLocation(latitude, longitude); + + for (int i = 0, n = projection.length; i < n; ++i) { + String column = projection[i]; + if (ImageColumns.DISPLAY_NAME.equals(column)) { + columnValues[i] = PicasaSource.getImageTitle(image); + } else if (ImageColumns.SIZE.equals(column)){ + columnValues[i] = PicasaSource.getImageSize(image); + } else if (ImageColumns.MIME_TYPE.equals(column)) { + columnValues[i] = PicasaSource.getContentType(image); + } else if (ImageColumns.DATE_TAKEN.equals(column)) { + columnValues[i] = PicasaSource.getDateTaken(image); + } else if (ImageColumns.LATITUDE.equals(column)) { + columnValues[i] = isValidLatlong ? latitude : null; + } else if (ImageColumns.LONGITUDE.equals(column)) { + columnValues[i] = isValidLatlong ? longitude : null; + } else if (ImageColumns.ORIENTATION.equals(column)) { + columnValues[i] = PicasaSource.getRotation(image); + } else { + Log.w(TAG, "unsupported column: " + column); + } + } + MatrixCursor cursor = new MatrixCursor(projection); + cursor.addRow(columnValues); + return cursor; + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) + throws FileNotFoundException { + long token = Binder.clearCallingIdentity(); + try { + if (mode.contains("w")) { + throw new FileNotFoundException("cannot open file for write"); + } + Path path = Path.fromString(uri.getPath()); + MediaObject object = mDataManager.getMediaObject(path); + if (object == null) { + throw new FileNotFoundException(uri.toString()); + } + if (PicasaSource.isPicasaImage(object)) { + return PicasaSource.openFile(getContext(), object, mode); + } else if (object instanceof MtpImage) { + return openPipeHelper(uri, null, null, null, + new MtpPipeDataWriter((MtpImage) object)); + } else { + throw new FileNotFoundException("unspported type: " + object); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + private final class MtpPipeDataWriter implements PipeDataWriter { + private final MtpImage mImage; + + private MtpPipeDataWriter(MtpImage image) { + mImage = image; + } + + @Override + public void writeDataToPipe(ParcelFileDescriptor output, + Uri uri, String mimeType, Bundle opts, Object args) { + OutputStream os = null; + try { + os = new ParcelFileDescriptor.AutoCloseOutputStream(output); + os.write(mImage.getImageData()); + } catch (IOException e) { + Log.w(TAG, "fail to download: " + uri, e); + } finally { + Utils.closeSilently(os); + } + } + } +} diff --git a/src/com/android/gallery3d/ui/AbstractDisplayItem.java b/src/com/android/gallery3d/ui/AbstractDisplayItem.java new file mode 100644 index 000000000..aad3919b5 --- /dev/null +++ b/src/com/android/gallery3d/ui/AbstractDisplayItem.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.data.MediaItem; + +import android.graphics.Bitmap; + +public abstract class AbstractDisplayItem extends DisplayItem { + + private static final String TAG = "AbstractDisplayItem"; + + private static final int STATE_INVALID = 0x01; + private static final int STATE_VALID = 0x02; + private static final int STATE_UPDATING = 0x04; + private static final int STATE_CANCELING = 0x08; + private static final int STATE_ERROR = 0x10; + + private int mState = STATE_INVALID; + private boolean mImageRequested = false; + private boolean mRecycling = false; + private Bitmap mBitmap; + + protected final MediaItem mMediaItem; + private int mRotation; + + public AbstractDisplayItem(MediaItem item) { + mMediaItem = item; + if (item == null) mState = STATE_ERROR; + if (item != null) mRotation = mMediaItem.getRotation(); + } + + protected void updateImage(Bitmap bitmap, boolean isCancelled) { + if (mRecycling) { + return; + } + + if (isCancelled && bitmap == null) { + mState = STATE_INVALID; + if (mImageRequested) { + // request image again. + requestImage(); + } + return; + } + + mBitmap = bitmap; + mState = bitmap == null ? STATE_ERROR : STATE_VALID ; + onBitmapAvailable(mBitmap); + } + + @Override + public int getRotation() { + return mRotation; + } + + @Override + public long getIdentity() { + return mMediaItem != null + ? System.identityHashCode(mMediaItem.getPath()) + : System.identityHashCode(this); + } + + public void requestImage() { + mImageRequested = true; + if (mState == STATE_INVALID) { + mState = STATE_UPDATING; + startLoadBitmap(); + } + } + + public void cancelImageRequest() { + mImageRequested = false; + if (mState == STATE_UPDATING) { + mState = STATE_CANCELING; + cancelLoadBitmap(); + } + } + + private boolean inState(int states) { + return (mState & states) != 0; + } + + public void recycle() { + if (!inState(STATE_UPDATING | STATE_CANCELING)) { + if (mBitmap != null) mBitmap = null; + } else { + mRecycling = true; + cancelImageRequest(); + } + } + + public boolean isRequestInProgress() { + return mImageRequested && inState(STATE_UPDATING | STATE_CANCELING); + } + + abstract protected void startLoadBitmap(); + abstract protected void cancelLoadBitmap(); + abstract protected void onBitmapAvailable(Bitmap bitmap); +} diff --git a/src/com/android/gallery3d/ui/ActionModeHandler.java b/src/com/android/gallery3d/ui/ActionModeHandler.java new file mode 100644 index 000000000..6c81a3f6a --- /dev/null +++ b/src/com/android/gallery3d/ui/ActionModeHandler.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryActionBar; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.CustomMenu.DropDownMenu; +import com.android.gallery3d.ui.MenuExecutor.ProgressListener; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Handler; +import android.view.ActionMode; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.PopupMenu.OnMenuItemClickListener; +import android.widget.ShareActionProvider; + +import java.util.ArrayList; + +public class ActionModeHandler implements ActionMode.Callback { + private static final String TAG = "ActionModeHandler"; + private static final int SUPPORT_MULTIPLE_MASK = MediaObject.SUPPORT_DELETE + | MediaObject.SUPPORT_ROTATE | MediaObject.SUPPORT_SHARE + | MediaObject.SUPPORT_CACHE | MediaObject.SUPPORT_IMPORT; + + public interface ActionModeListener { + public boolean onActionItemClicked(MenuItem item); + } + + private final GalleryActivity mActivity; + private final MenuExecutor mMenuExecutor; + private final SelectionManager mSelectionManager; + private Menu mMenu; + private DropDownMenu mSelectionMenu; + private ActionModeListener mListener; + private Future mMenuTask; + private Handler mMainHandler; + private ShareActionProvider mShareActionProvider; + + public ActionModeHandler( + GalleryActivity activity, SelectionManager selectionManager) { + mActivity = Utils.checkNotNull(activity); + mSelectionManager = Utils.checkNotNull(selectionManager); + mMenuExecutor = new MenuExecutor(activity, selectionManager); + mMainHandler = new Handler(activity.getMainLooper()); + } + + public ActionMode startActionMode() { + Activity a = (Activity) mActivity; + final ActionMode actionMode = a.startActionMode(this); + CustomMenu customMenu = new CustomMenu(a); + View customView = LayoutInflater.from(a).inflate( + R.layout.action_mode, null); + actionMode.setCustomView(customView); + mSelectionMenu = customMenu.addDropDownMenu( + (Button) customView.findViewById(R.id.selection_menu), + R.menu.selection); + customMenu.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + return onActionItemClicked(actionMode, item); + } + }); + return actionMode; + } + + public void setTitle(String title) { + mSelectionMenu.setTitle(title); + } + + public void setActionModeListener(ActionModeListener listener) { + mListener = listener; + } + + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + boolean result; + if (mListener != null) { + result = mListener.onActionItemClicked(item); + if (result) { + mSelectionManager.leaveSelectionMode(); + return result; + } + } + ProgressListener listener = null; + if (item.getItemId() == R.id.action_import) { + listener = new ImportCompleteListener(mActivity); + } + result = mMenuExecutor.onMenuClicked(item, listener); + if (item.getItemId() == R.id.action_select_all) { + updateSupportedOperation(); + + // For clients who call SelectionManager.selectAll() directly, we need to ensure the + // menu status is consistent with selection manager. + item = mSelectionMenu.findItem(R.id.action_select_all); + if (item != null) { + if (mSelectionManager.inSelectAllMode()) { + item.setChecked(true); + item.setTitle(R.string.deselect_all); + } else { + item.setChecked(false); + item.setTitle(R.string.select_all); + } + } + } + return result; + } + + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.operation, menu); + + mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu); + + mMenu = menu; + return true; + } + + public void onDestroyActionMode(ActionMode mode) { + mSelectionManager.leaveSelectionMode(); + } + + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return true; + } + + private void updateMenuOptionsAndSharingIntent(JobContext jc) { + ArrayList paths = mSelectionManager.getSelected(true); + if (paths.size() == 0) return; + + int operation = MediaObject.SUPPORT_ALL; + DataManager manager = mActivity.getDataManager(); + final ArrayList uris = new ArrayList(); + int type = 0; + for (Path path : paths) { + if (jc.isCancelled()) return; + int support = manager.getSupportedOperations(path); + type |= manager.getMediaType(path); + operation &= support; + if ((support & MediaObject.SUPPORT_SHARE) != 0) { + uris.add(manager.getContentUri(path)); + } + } + final Intent intent = new Intent(); + final String mimeType = MenuExecutor.getMimeType(type); + + if (paths.size() == 1) { + if (!GalleryUtils.isEditorAvailable((Context) mActivity, mimeType)) { + operation &= ~MediaObject.SUPPORT_EDIT; + } + } else { + operation &= SUPPORT_MULTIPLE_MASK; + } + + + Log.v(TAG, "Sharing intent MIME type=" + mimeType + ", uri size = "+ uris.size()); + if (uris.size() > 1) { + intent.setAction(Intent.ACTION_SEND_MULTIPLE).setType(mimeType); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + } else { + intent.setAction(Intent.ACTION_SEND).setType(mimeType); + intent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); + } + intent.setType(mimeType); + + final int supportedOperation = operation; + + mMainHandler.post(new Runnable() { + @Override + public void run() { + mMenuTask = null; + MenuExecutor.updateMenuOperation(mMenu, supportedOperation); + + if (mShareActionProvider != null) { + Log.v(TAG, "Sharing intent is ready: action = " + intent.getAction()); + mShareActionProvider.setShareIntent(intent); + } + } + }); + } + + public void updateSupportedOperation(Path path, boolean selected) { + // TODO: We need to improve the performance + updateSupportedOperation(); + } + + public void updateSupportedOperation() { + if (mMenuTask != null) { + mMenuTask.cancel(); + } + + // Disable share action until share intent is in good shape + if (mShareActionProvider != null) { + Log.v(TAG, "Disable sharing until intent is ready"); + mShareActionProvider.setShareIntent(null); + } + + // Generate sharing intent and update supported operations in the background + mMenuTask = mActivity.getThreadPool().submit(new Job() { + public Void run(JobContext jc) { + updateMenuOptionsAndSharingIntent(jc); + return null; + } + }); + } + + public void pause() { + if (mMenuTask != null) { + mMenuTask.cancel(); + mMenuTask = null; + } + mMenuExecutor.pause(); + } + + public void resume() { + updateSupportedOperation(); + } +} diff --git a/src/com/android/gallery3d/ui/AdaptiveBackground.java b/src/com/android/gallery3d/ui/AdaptiveBackground.java new file mode 100644 index 000000000..42cb2ccdb --- /dev/null +++ b/src/com/android/gallery3d/ui/AdaptiveBackground.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.LightingColorFilter; +import android.graphics.Paint; + +import com.android.gallery3d.anim.FloatAnimation; + +public class AdaptiveBackground extends GLView { + + private static final int BACKGROUND_WIDTH = 128; + private static final int BACKGROUND_HEIGHT = 64; + private static final int FILTERED_COLOR = 0xffaaaaaa; + private static final int ANIMATION_DURATION = 500; + + private BasicTexture mOldBackground; + private BasicTexture mBackground; + + private final Paint mPaint; + private Bitmap mPendingBitmap; + private final FloatAnimation mAnimation = + new FloatAnimation(0, 1, ANIMATION_DURATION); + + public AdaptiveBackground() { + Paint paint = new Paint(); + paint.setFilterBitmap(true); + paint.setColorFilter(new LightingColorFilter(FILTERED_COLOR, 0)); + mPaint = paint; + } + + public Bitmap getAdaptiveBitmap(Bitmap bitmap) { + Bitmap target = Bitmap.createBitmap( + BACKGROUND_WIDTH, BACKGROUND_HEIGHT, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(target); + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + int left = 0; + int top = 0; + if (width * BACKGROUND_HEIGHT > height * BACKGROUND_WIDTH) { + float scale = (float) BACKGROUND_HEIGHT / height; + canvas.scale(scale, scale); + left = (BACKGROUND_WIDTH - (int) (width * scale + 0.5)) / 2; + } else { + float scale = (float) BACKGROUND_WIDTH / width; + canvas.scale(scale, scale); + top = (BACKGROUND_HEIGHT - (int) (height * scale + 0.5)) / 2; + } + canvas.drawBitmap(bitmap, left, top, mPaint); + BoxBlurFilter.apply(target, + BoxBlurFilter.MODE_REPEAT, BoxBlurFilter.MODE_CLAMP); + return target; + } + + private void startTransition(Bitmap bitmap) { + BitmapTexture texture = new BitmapTexture(bitmap); + if (mBackground == null) { + mBackground = texture; + } else { + if (mOldBackground != null) mOldBackground.recycle(); + mOldBackground = mBackground; + mBackground = texture; + mAnimation.start(); + } + invalidate(); + } + + public void setImage(Bitmap bitmap) { + if (mAnimation.isActive()) { + mPendingBitmap = bitmap; + } else { + startTransition(bitmap); + } + } + + public void setScrollPosition(int position) { + if (mScrollX == position) return; + mScrollX = position; + invalidate(); + } + + @Override + protected void render(GLCanvas canvas) { + if (mBackground == null) return; + + int height = getHeight(); + float scale = (float) height / BACKGROUND_HEIGHT; + int width = (int) (BACKGROUND_WIDTH * scale + 0.5f); + int scroll = mScrollX; + int start = (scroll / width) * width; + + if (mOldBackground == null) { + for (int i = start, n = scroll + getWidth(); i < n; i += width) { + mBackground.draw(canvas, i - scroll, 0, width, height); + } + } else { + boolean moreAnimation = + mAnimation.calculate(canvas.currentAnimationTimeMillis()); + float ratio = mAnimation.get(); + for (int i = start, n = scroll + getWidth(); i < n; i += width) { + canvas.drawMixed(mOldBackground, + mBackground, ratio, i - scroll, 0, width, height); + } + if (moreAnimation) { + invalidate(); + } else if (mPendingBitmap != null) { + startTransition(mPendingBitmap); + mPendingBitmap = null; + } + } + } +} diff --git a/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java new file mode 100644 index 000000000..92d8b4156 --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java @@ -0,0 +1,543 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.AlbumSetView.AlbumSetItem; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.MediaSetUtils; +import com.android.gallery3d.util.ThreadPool; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.os.Message; + +public class AlbumSetSlidingWindow implements AlbumSetView.ModelListener { + private static final String TAG = "GallerySlidingWindow"; + private static final int MSG_LOAD_BITMAP_DONE = 0; + + public static interface Listener { + public void onSizeChanged(int size); + public void onContentInvalidated(); + public void onWindowContentChanged( + int slot, AlbumSetItem old, AlbumSetItem update); + } + + private final AlbumSetView.Model mSource; + private int mSize; + private int mLabelWidth; + private int mDisplayItemSize; + private int mLabelFontSize; + + private int mContentStart = 0; + private int mContentEnd = 0; + + private int mActiveStart = 0; + private int mActiveEnd = 0; + + private Listener mListener; + + private final MyAlbumSetItem mData[]; + private SelectionDrawer mSelectionDrawer; + private final ColorTexture mWaitLoadingTexture; + + private SynchronizedHandler mHandler; + private ThreadPool mThreadPool; + + private int mActiveRequestCount = 0; + private String mLoadingLabel; + private boolean mIsActive = false; + + private static class MyAlbumSetItem extends AlbumSetItem { + public Path setPath; + public int sourceType; + public int cacheFlag; + public int cacheStatus; + } + + public AlbumSetSlidingWindow(GalleryActivity activity, int labelWidth, + int displayItemSize, int labelFontSize, SelectionDrawer drawer, + AlbumSetView.Model source, int cacheSize) { + source.setModelListener(this); + mLabelWidth = labelWidth; + mDisplayItemSize = displayItemSize; + mLabelFontSize = labelFontSize; + mLoadingLabel = activity.getAndroidContext().getString(R.string.loading); + mSource = source; + mSelectionDrawer = drawer; + mData = new MyAlbumSetItem[cacheSize]; + mSize = source.size(); + + mWaitLoadingTexture = new ColorTexture(Color.TRANSPARENT); + mWaitLoadingTexture.setSize(1, 1); + + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + Utils.assertTrue(message.what == MSG_LOAD_BITMAP_DONE); + ((GalleryDisplayItem) message.obj).onLoadBitmapDone(); + } + }; + + mThreadPool = activity.getThreadPool(); + } + + public void setSelectionDrawer(SelectionDrawer drawer) { + mSelectionDrawer = drawer; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public AlbumSetItem get(int slotIndex) { + Utils.assertTrue(isActiveSlot(slotIndex), + "invalid slot: %s outsides (%s, %s)", + slotIndex, mActiveStart, mActiveEnd); + return mData[slotIndex % mData.length]; + } + + public int size() { + return mSize; + } + + public boolean isActiveSlot(int slotIndex) { + return slotIndex >= mActiveStart && slotIndex < mActiveEnd; + } + + private void setContentWindow(int contentStart, int contentEnd) { + if (contentStart == mContentStart && contentEnd == mContentEnd) return; + + if (contentStart >= mContentEnd || mContentStart >= contentEnd) { + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mSource.setActiveWindow(contentStart, contentEnd); + for (int i = contentStart; i < contentEnd; ++i) { + prepareSlotContent(i); + } + } else { + for (int i = mContentStart; i < contentStart; ++i) { + freeSlotContent(i); + } + for (int i = contentEnd, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mSource.setActiveWindow(contentStart, contentEnd); + for (int i = contentStart, n = mContentStart; i < n; ++i) { + prepareSlotContent(i); + } + for (int i = mContentEnd; i < contentEnd; ++i) { + prepareSlotContent(i); + } + } + + mContentStart = contentStart; + mContentEnd = contentEnd; + } + + public void setActiveWindow(int start, int end) { + Utils.assertTrue( + start <= end && end - start <= mData.length && end <= mSize, + "start = %s, end = %s, length = %s, size = %s", + start, end, mData.length, mSize); + + AlbumSetItem data[] = mData; + + mActiveStart = start; + mActiveEnd = end; + + // If no data is visible, keep the cache content + if (start == end) return; + + int contentStart = Utils.clamp((start + end) / 2 - data.length / 2, + 0, Math.max(0, mSize - data.length)); + int contentEnd = Math.min(contentStart + data.length, mSize); + setContentWindow(contentStart, contentEnd); + if (mIsActive) updateAllImageRequests(); + } + + // We would like to request non active slots in the following order: + // Order: 8 6 4 2 1 3 5 7 + // |---------|---------------|---------| + // |<- active ->| + // |<-------- cached range ----------->| + private void requestNonactiveImages() { + int range = Math.max( + mContentEnd - mActiveEnd, mActiveStart - mContentStart); + for (int i = 0 ;i < range; ++i) { + requestImagesInSlot(mActiveEnd + i); + requestImagesInSlot(mActiveStart - 1 - i); + } + } + + private void cancelNonactiveImages() { + int range = Math.max( + mContentEnd - mActiveEnd, mActiveStart - mContentStart); + for (int i = 0 ;i < range; ++i) { + cancelImagesInSlot(mActiveEnd + i); + cancelImagesInSlot(mActiveStart - 1 - i); + } + } + + private void requestImagesInSlot(int slotIndex) { + if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; + AlbumSetItem items = mData[slotIndex % mData.length]; + for (DisplayItem item : items.covers) { + ((GalleryDisplayItem) item).requestImage(); + } + } + + private void cancelImagesInSlot(int slotIndex) { + if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; + AlbumSetItem items = mData[slotIndex % mData.length]; + for (DisplayItem item : items.covers) { + ((GalleryDisplayItem) item).cancelImageRequest(); + } + } + + private void freeSlotContent(int slotIndex) { + AlbumSetItem data[] = mData; + int index = slotIndex % data.length; + AlbumSetItem original = data[index]; + if (original != null) { + data[index] = null; + for (DisplayItem item : original.covers) { + ((GalleryDisplayItem) item).recycle(); + } + } + } + + private long getMediaSetDataVersion(MediaSet set) { + return set == null + ? MediaSet.INVALID_DATA_VERSION + : set.getDataVersion(); + } + + private void prepareSlotContent(int slotIndex) { + MediaSet set = mSource.getMediaSet(slotIndex); + + MyAlbumSetItem item = new MyAlbumSetItem(); + MediaItem[] coverItems = mSource.getCoverItems(slotIndex); + item.covers = new GalleryDisplayItem[coverItems.length]; + item.sourceType = identifySourceType(set); + item.cacheFlag = identifyCacheFlag(set); + item.cacheStatus = identifyCacheStatus(set); + item.setPath = set == null ? null : set.getPath(); + + for (int i = 0; i < coverItems.length; ++i) { + item.covers[i] = new GalleryDisplayItem(slotIndex, i, coverItems[i]); + } + item.labelItem = new LabelDisplayItem(slotIndex); + item.setDataVersion = getMediaSetDataVersion(set); + mData[slotIndex % mData.length] = item; + } + + private boolean isCoverItemsChanged(int slotIndex) { + AlbumSetItem original = mData[slotIndex % mData.length]; + if (original == null) return true; + MediaItem[] coverItems = mSource.getCoverItems(slotIndex); + + if (original.covers.length != coverItems.length) return true; + for (int i = 0, n = coverItems.length; i < n; ++i) { + GalleryDisplayItem g = (GalleryDisplayItem) original.covers[i]; + if (g.mDataVersion != coverItems[i].getDataVersion()) return true; + } + return false; + } + + private void updateSlotContent(final int slotIndex) { + + MyAlbumSetItem data[] = mData; + int pos = slotIndex % data.length; + MyAlbumSetItem original = data[pos]; + + if (!isCoverItemsChanged(slotIndex)) { + MediaSet set = mSource.getMediaSet(slotIndex); + original.sourceType = identifySourceType(set); + original.cacheFlag = identifyCacheFlag(set); + original.cacheStatus = identifyCacheStatus(set); + original.setPath = set == null ? null : set.getPath(); + ((LabelDisplayItem) original.labelItem).updateContent(); + if (mListener != null) mListener.onContentInvalidated(); + return; + } + + prepareSlotContent(slotIndex); + AlbumSetItem update = data[pos]; + + if (mListener != null && isActiveSlot(slotIndex)) { + mListener.onWindowContentChanged(slotIndex, original, update); + } + if (original != null) { + for (DisplayItem item : original.covers) { + ((GalleryDisplayItem) item).recycle(); + } + } + } + + private void notifySlotChanged(int slotIndex) { + // If the updated content is not cached, ignore it + if (slotIndex < mContentStart || slotIndex >= mContentEnd) { + Log.w(TAG, String.format( + "invalid update: %s is outside (%s, %s)", + slotIndex, mContentStart, mContentEnd) ); + return; + } + updateSlotContent(slotIndex); + boolean isActiveSlot = isActiveSlot(slotIndex); + if (mActiveRequestCount == 0 || isActiveSlot) { + for (DisplayItem item : mData[slotIndex % mData.length].covers) { + GalleryDisplayItem galleryItem = (GalleryDisplayItem) item; + galleryItem.requestImage(); + if (isActiveSlot && galleryItem.isRequestInProgress()) { + ++mActiveRequestCount; + } + } + } + } + + private void updateAllImageRequests() { + mActiveRequestCount = 0; + for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { + for (DisplayItem item : mData[i % mData.length].covers) { + GalleryDisplayItem coverItem = (GalleryDisplayItem) item; + coverItem.requestImage(); + if (coverItem.isRequestInProgress()) ++mActiveRequestCount; + } + } + if (mActiveRequestCount == 0) { + requestNonactiveImages(); + } else { + cancelNonactiveImages(); + } + } + + private class GalleryDisplayItem extends AbstractDisplayItem + implements FutureListener { + private Future mFuture; + private final int mSlotIndex; + private final int mCoverIndex; + private final int mMediaType; + private Texture mContent; + private final long mDataVersion; + + public GalleryDisplayItem(int slotIndex, int coverIndex, MediaItem item) { + super(item); + mSlotIndex = slotIndex; + mCoverIndex = coverIndex; + mMediaType = item.getMediaType(); + mDataVersion = item.getDataVersion(); + updateContent(mWaitLoadingTexture); + } + + @Override + protected void onBitmapAvailable(Bitmap bitmap) { + if (isActiveSlot(mSlotIndex)) { + --mActiveRequestCount; + if (mActiveRequestCount == 0) requestNonactiveImages(); + } + if (bitmap != null) { + BitmapTexture texture = new BitmapTexture(bitmap); + texture.setThrottled(true); + updateContent(texture); + if (mListener != null) mListener.onContentInvalidated(); + } + } + + private void updateContent(Texture content) { + mContent = content; + + int width = content.getWidth(); + int height = content.getHeight(); + + float scale = (float) mDisplayItemSize / Math.max(width, height); + + width = (int) Math.floor(width * scale); + height = (int) Math.floor(height * scale); + + setSize(width, height); + } + + @Override + public boolean render(GLCanvas canvas, int pass) { + int sourceType = SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED; + int cacheFlag = MediaSet.CACHE_FLAG_NO; + int cacheStatus = MediaSet.CACHE_STATUS_NOT_CACHED; + MyAlbumSetItem set = mData[mSlotIndex % mData.length]; + Path path = set.setPath; + if (mCoverIndex == 0) { + sourceType = set.sourceType; + cacheFlag = set.cacheFlag; + cacheStatus = set.cacheStatus; + } + + mSelectionDrawer.draw(canvas, mContent, mWidth, mHeight, + getRotation(), path, mCoverIndex, sourceType, mMediaType, + cacheFlag == MediaSet.CACHE_FLAG_FULL, + (cacheFlag == MediaSet.CACHE_FLAG_FULL) + && (cacheStatus != MediaSet.CACHE_STATUS_CACHED_FULL)); + return false; + } + + @Override + public void startLoadBitmap() { + mFuture = mThreadPool.submit(mMediaItem.requestImage( + MediaItem.TYPE_MICROTHUMBNAIL), this); + } + + @Override + public void cancelLoadBitmap() { + mFuture.cancel(); + } + + @Override + public void onFutureDone(Future future) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_LOAD_BITMAP_DONE, this)); + } + + private void onLoadBitmapDone() { + Future future = mFuture; + mFuture = null; + updateImage(future.get(), future.isCancelled()); + } + + @Override + public String toString() { + return String.format("GalleryDisplayItem(%s, %s)", mSlotIndex, mCoverIndex); + } + } + + private static int identifySourceType(MediaSet set) { + if (set == null) { + return SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED; + } + + Path path = set.getPath(); + if (MediaSetUtils.isCameraSource(path)) { + return SelectionDrawer.DATASOURCE_TYPE_CAMERA; + } + + int type = SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED; + String prefix = path.getPrefix(); + + if (prefix.equals("picasa")) { + type = SelectionDrawer.DATASOURCE_TYPE_PICASA; + } else if (prefix.equals("local") || prefix.equals("merge")) { + type = SelectionDrawer.DATASOURCE_TYPE_LOCAL; + } else if (prefix.equals("mtp")) { + type = SelectionDrawer.DATASOURCE_TYPE_MTP; + } + + return type; + } + + private static int identifyCacheFlag(MediaSet set) { + if (set == null || (set.getSupportedOperations() + & MediaSet.SUPPORT_CACHE) == 0) { + return MediaSet.CACHE_FLAG_NO; + } + + return set.getCacheFlag(); + } + + private static int identifyCacheStatus(MediaSet set) { + if (set == null || (set.getSupportedOperations() + & MediaSet.SUPPORT_CACHE) == 0) { + return MediaSet.CACHE_STATUS_NOT_CACHED; + } + + return set.getCacheStatus(); + } + + private class LabelDisplayItem extends DisplayItem { + private static final int FONT_COLOR = Color.WHITE; + + private StringTexture mTexture; + private String mLabel; + private String mPostfix; + private final int mSlotIndex; + + public LabelDisplayItem(int slotIndex) { + mSlotIndex = slotIndex; + updateContent(); + } + + public boolean updateContent() { + String label = mLoadingLabel; + String postfix = null; + MediaSet set = mSource.getMediaSet(mSlotIndex); + if (set != null) { + label = Utils.ensureNotNull(set.getName()); + postfix = " (" + set.getTotalMediaItemCount() + ")"; + } + if (Utils.equals(label, mLabel) + && Utils.equals(postfix, mPostfix)) return false; + mTexture = StringTexture.newInstance( + label, postfix, mLabelFontSize, FONT_COLOR, mLabelWidth, true); + setSize(mTexture.getWidth(), mTexture.getHeight()); + return true; + } + + @Override + public boolean render(GLCanvas canvas, int pass) { + mTexture.draw(canvas, -mWidth / 2, -mHeight / 2); + return false; + } + + @Override + public long getIdentity() { + return System.identityHashCode(this); + } + } + + public void onSizeChanged(int size) { + if (mSize != size) { + mSize = size; + if (mListener != null && mIsActive) mListener.onSizeChanged(mSize); + } + } + + public void onWindowContentChanged(int index) { + if (!mIsActive) { + // paused, ignore slot changed event + return; + } + notifySlotChanged(index); + } + + public void pause() { + mIsActive = false; + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + } + + public void resume() { + mIsActive = true; + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + prepareSlotContent(i); + } + updateAllImageRequests(); + } +} diff --git a/src/com/android/gallery3d/ui/AlbumSetView.java b/src/com/android/gallery3d/ui/AlbumSetView.java new file mode 100644 index 000000000..ef066b34c --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumSetView.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.ui.PositionRepository.Position; + +import android.graphics.Rect; + +import java.util.Random; + +public class AlbumSetView extends SlotView { + @SuppressWarnings("unused") + private static final String TAG = "AlbumSetView"; + private static final int CACHE_SIZE = 32; + private static final float PHOTO_DISTANCE = 35f; + + private int mVisibleStart; + private int mVisibleEnd; + + private Random mRandom = new Random(); + private long mSeed = mRandom.nextLong(); + + private AlbumSetSlidingWindow mDataWindow; + private final GalleryActivity mActivity; + private final int mSlotWidth; + private final int mDisplayItemSize; + private final int mLabelFontSize; + private final int mLabelOffsetY; + private final int mLabelMargin; + + private SelectionDrawer mSelectionDrawer; + + public static interface Model { + public MediaItem[] getCoverItems(int index); + public MediaSet getMediaSet(int index); + public int size(); + public void setActiveWindow(int start, int end); + public void setModelListener(ModelListener listener); + } + + public static interface ModelListener { + public void onWindowContentChanged(int index); + public void onSizeChanged(int size); + } + + public static class AlbumSetItem { + public DisplayItem[] covers; + public DisplayItem labelItem; + public long setDataVersion; + } + + public AlbumSetView(GalleryActivity activity, SelectionDrawer drawer, + int slotWidth, int slotHeight, int displayItemSize, + int labelFontSize, int labelOffsetY, int labelMargin) { + super(activity.getAndroidContext()); + mActivity = activity; + setSelectionDrawer(drawer); + setSlotSize(slotWidth, slotHeight); + mSlotWidth = slotWidth; + mDisplayItemSize = displayItemSize; + mLabelFontSize = labelFontSize; + mLabelOffsetY = labelOffsetY; + mLabelMargin = labelMargin; + } + + public void setSelectionDrawer(SelectionDrawer drawer) { + mSelectionDrawer = drawer; + if (mDataWindow != null) { + mDataWindow.setSelectionDrawer(drawer); + } + } + + public void setModel(AlbumSetView.Model model) { + if (mDataWindow != null) { + mDataWindow.setListener(null); + setSlotCount(0); + mDataWindow = null; + } + if (model != null) { + mDataWindow = new AlbumSetSlidingWindow(mActivity, + mSlotWidth - mLabelMargin * 2, mDisplayItemSize, mLabelFontSize, + mSelectionDrawer, model, CACHE_SIZE); + mDataWindow.setListener(new MyCacheListener()); + setSlotCount(mDataWindow.size()); + updateVisibleRange(getVisibleStart(), getVisibleEnd()); + } + } + + private void putSlotContent(int slotIndex, AlbumSetItem entry) { + // Get displayItems from mItemsetMap or create them from MediaSet. + Utils.assertTrue(entry != null); + Rect rect = getSlotRect(slotIndex); + + DisplayItem[] items = entry.covers; + mRandom.setSeed(slotIndex ^ mSeed); + + int x = (rect.left + rect.right) / 2; + int y = (rect.top + rect.bottom) / 2; + + Position basePosition = new Position(x, y, 0); + + // Put the cover items in reverse order, so that the first item is on + // top of the rest. + int labelY = y + mLabelOffsetY - entry.labelItem.getHeight() / 2; + Position position = new Position(x, labelY, 0f); + putDisplayItem(position, position, entry.labelItem); + + for (int i = 0, n = items.length; i < n; ++i) { + DisplayItem item = items[i]; + float dx = 0; + float dy = 0; + float dz = 0f; + float theta = 0; + if (i != 0) { + dz = i * PHOTO_DISTANCE; + } + position = new Position(x + dx, y + dy, dz); + position.theta = theta; + putDisplayItem(position, basePosition, item); + } + + } + + private void freeSlotContent(int index, AlbumSetItem entry) { + if (entry == null) return; + for (DisplayItem item : entry.covers) { + removeDisplayItem(item); + } + removeDisplayItem(entry.labelItem); + } + + public int size() { + return mDataWindow.size(); + } + + @Override + public void onLayoutChanged(int width, int height) { + updateVisibleRange(0, 0); + updateVisibleRange(getVisibleStart(), getVisibleEnd()); + } + + @Override + public void onScrollPositionChanged(int position) { + super.onScrollPositionChanged(position); + updateVisibleRange(getVisibleStart(), getVisibleEnd()); + } + + private void updateVisibleRange(int start, int end) { + if (start == mVisibleStart && end == mVisibleEnd) { + // we need to set the mDataWindow active range in any case. + mDataWindow.setActiveWindow(start, end); + return; + } + if (start >= mVisibleEnd || mVisibleStart >= end) { + for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) { + freeSlotContent(i, mDataWindow.get(i)); + } + mDataWindow.setActiveWindow(start, end); + for (int i = start; i < end; ++i) { + putSlotContent(i, mDataWindow.get(i)); + } + } else { + for (int i = mVisibleStart; i < start; ++i) { + freeSlotContent(i, mDataWindow.get(i)); + } + for (int i = end, n = mVisibleEnd; i < n; ++i) { + freeSlotContent(i, mDataWindow.get(i)); + } + mDataWindow.setActiveWindow(start, end); + for (int i = start, n = mVisibleStart; i < n; ++i) { + putSlotContent(i, mDataWindow.get(i)); + } + for (int i = mVisibleEnd; i < end; ++i) { + putSlotContent(i, mDataWindow.get(i)); + } + } + mVisibleStart = start; + mVisibleEnd = end; + + invalidate(); + } + + @Override + protected void render(GLCanvas canvas) { + mSelectionDrawer.prepareDrawing(); + super.render(canvas); + } + + private class MyCacheListener implements AlbumSetSlidingWindow.Listener { + + public void onSizeChanged(int size) { + // If the layout parameters are changed, we need reput all items. + if (setSlotCount(size)) updateVisibleRange(0, 0); + updateVisibleRange(getVisibleStart(), getVisibleEnd()); + invalidate(); + } + + public void onWindowContentChanged(int slot, AlbumSetItem old, AlbumSetItem update) { + freeSlotContent(slot, old); + putSlotContent(slot, update); + invalidate(); + } + + public void onContentInvalidated() { + invalidate(); + } + } + + public void pause() { + for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) { + freeSlotContent(i, mDataWindow.get(i)); + } + mDataWindow.pause(); + } + + public void resume() { + mDataWindow.resume(); + for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) { + putSlotContent(i, mDataWindow.get(i)); + } + } +} diff --git a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java new file mode 100644 index 000000000..9e44bd1d2 --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java @@ -0,0 +1,433 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.LruCache; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.os.Message; + +public class AlbumSlidingWindow implements AlbumView.ModelListener { + @SuppressWarnings("unused") + private static final String TAG = "AlbumSlidingWindow"; + + private static final int MSG_LOAD_BITMAP_DONE = 0; + private static final int MSG_UPDATE_SLOT = 1; + private static final int MIN_THUMB_SIZE = 100; + + public static interface Listener { + public void onSizeChanged(int size); + public void onContentInvalidated(); + public void onWindowContentChanged( + int slot, DisplayItem old, DisplayItem update); + } + + private final AlbumView.Model mSource; + private int mSize; + + private int mContentStart = 0; + private int mContentEnd = 0; + + private int mActiveStart = 0; + private int mActiveEnd = 0; + + private Listener mListener; + private int mFocusIndex = -1; + + private final AlbumDisplayItem mData[]; + private final ColorTexture mWaitLoadingTexture; + private SelectionDrawer mSelectionDrawer; + + private SynchronizedHandler mHandler; + private ThreadPool mThreadPool; + private int mSlotWidth, mSlotHeight; + + private int mActiveRequestCount = 0; + private boolean mIsActive = false; + + private int mDisplayItemSize; // 0: disabled + private LruCache mImageCache = new LruCache(1000); + + public AlbumSlidingWindow(GalleryActivity activity, + AlbumView.Model source, int cacheSize, + int slotWidth, int slotHeight, int displayItemSize) { + source.setModelListener(this); + mSource = source; + mData = new AlbumDisplayItem[cacheSize]; + mSize = source.size(); + mSlotWidth = slotWidth; + mSlotHeight = slotHeight; + mDisplayItemSize = displayItemSize; + + mWaitLoadingTexture = new ColorTexture(Color.TRANSPARENT); + mWaitLoadingTexture.setSize(1, 1); + + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_LOAD_BITMAP_DONE: { + ((AlbumDisplayItem) message.obj).onLoadBitmapDone(); + break; + } + case MSG_UPDATE_SLOT: { + updateSlotContent(message.arg1); + break; + } + } + } + }; + + mThreadPool = activity.getThreadPool(); + } + + public void setSelectionDrawer(SelectionDrawer drawer) { + mSelectionDrawer = drawer; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public void setFocusIndex(int slotIndex) { + mFocusIndex = slotIndex; + } + + public DisplayItem get(int slotIndex) { + Utils.assertTrue(isActiveSlot(slotIndex), + "invalid slot: %s outsides (%s, %s)", + slotIndex, mActiveStart, mActiveEnd); + return mData[slotIndex % mData.length]; + } + + public int size() { + return mSize; + } + + public boolean isActiveSlot(int slotIndex) { + return slotIndex >= mActiveStart && slotIndex < mActiveEnd; + } + + private void setContentWindow(int contentStart, int contentEnd) { + if (contentStart == mContentStart && contentEnd == mContentEnd) return; + + if (!mIsActive) { + mContentStart = contentStart; + mContentEnd = contentEnd; + mSource.setActiveWindow(contentStart, contentEnd); + return; + } + + if (contentStart >= mContentEnd || mContentStart >= contentEnd) { + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mSource.setActiveWindow(contentStart, contentEnd); + for (int i = contentStart; i < contentEnd; ++i) { + prepareSlotContent(i); + } + } else { + for (int i = mContentStart; i < contentStart; ++i) { + freeSlotContent(i); + } + for (int i = contentEnd, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mSource.setActiveWindow(contentStart, contentEnd); + for (int i = contentStart, n = mContentStart; i < n; ++i) { + prepareSlotContent(i); + } + for (int i = mContentEnd; i < contentEnd; ++i) { + prepareSlotContent(i); + } + } + + mContentStart = contentStart; + mContentEnd = contentEnd; + } + + public void setActiveWindow(int start, int end) { + Utils.assertTrue(start <= end + && end - start <= mData.length && end <= mSize, + "%s, %s, %s, %s", start, end, mData.length, mSize); + DisplayItem data[] = mData; + + mActiveStart = start; + mActiveEnd = end; + + // If no data is visible, keep the cache content + if (start == end) return; + + int contentStart = Utils.clamp((start + end) / 2 - data.length / 2, + 0, Math.max(0, mSize - data.length)); + int contentEnd = Math.min(contentStart + data.length, mSize); + setContentWindow(contentStart, contentEnd); + if (mIsActive) updateAllImageRequests(); + } + + // We would like to request non active slots in the following order: + // Order: 8 6 4 2 1 3 5 7 + // |---------|---------------|---------| + // |<- active ->| + // |<-------- cached range ----------->| + private void requestNonactiveImages() { + int range = Math.max( + (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); + for (int i = 0 ;i < range; ++i) { + requestSlotImage(mActiveEnd + i, false); + requestSlotImage(mActiveStart - 1 - i, false); + } + } + + private void requestSlotImage(int slotIndex, boolean isActive) { + if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; + AlbumDisplayItem item = mData[slotIndex % mData.length]; + item.requestImage(); + } + + private void cancelNonactiveImages() { + int range = Math.max( + (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); + for (int i = 0 ;i < range; ++i) { + cancelSlotImage(mActiveEnd + i, false); + cancelSlotImage(mActiveStart - 1 - i, false); + } + } + + private void cancelSlotImage(int slotIndex, boolean isActive) { + if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; + AlbumDisplayItem item = mData[slotIndex % mData.length]; + item.cancelImageRequest(); + } + + private void freeSlotContent(int slotIndex) { + AlbumDisplayItem data[] = mData; + int index = slotIndex % data.length; + AlbumDisplayItem original = data[index]; + if (original != null) { + original.recycle(); + data[index] = null; + } + } + + private void prepareSlotContent(final int slotIndex) { + mData[slotIndex % mData.length] = new AlbumDisplayItem( + slotIndex, mSource.get(slotIndex)); + } + + private void updateSlotContent(final int slotIndex) { + MediaItem item = mSource.get(slotIndex); + AlbumDisplayItem data[] = mData; + int index = slotIndex % data.length; + AlbumDisplayItem original = data[index]; + AlbumDisplayItem update = new AlbumDisplayItem(slotIndex, item); + data[index] = update; + boolean isActive = isActiveSlot(slotIndex); + if (mListener != null && isActive) { + mListener.onWindowContentChanged(slotIndex, original, update); + } + if (original != null) { + if (isActive && original.isRequestInProgress()) { + --mActiveRequestCount; + } + original.recycle(); + } + if (isActive) { + if (mActiveRequestCount == 0) cancelNonactiveImages(); + ++mActiveRequestCount; + update.requestImage(); + } else { + if (mActiveRequestCount == 0) update.requestImage(); + } + } + + private void updateAllImageRequests() { + mActiveRequestCount = 0; + AlbumDisplayItem data[] = mData; + for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { + AlbumDisplayItem item = data[i % data.length]; + item.requestImage(); + if (item.isRequestInProgress()) ++mActiveRequestCount; + } + if (mActiveRequestCount == 0) { + requestNonactiveImages(); + } else { + cancelNonactiveImages(); + } + } + + private class AlbumDisplayItem extends AbstractDisplayItem + implements FutureListener, Job { + private Future mFuture; + private final int mSlotIndex; + private final int mMediaType; + private Texture mContent; + + public AlbumDisplayItem(int slotIndex, MediaItem item) { + super(item); + mMediaType = (item == null) + ? MediaItem.MEDIA_TYPE_UNKNOWN + : item.getMediaType(); + mSlotIndex = slotIndex; + updateContent(mWaitLoadingTexture); + } + + @Override + protected void onBitmapAvailable(Bitmap bitmap) { + boolean isActiveSlot = isActiveSlot(mSlotIndex); + if (isActiveSlot) { + --mActiveRequestCount; + if (mActiveRequestCount == 0) requestNonactiveImages(); + } + if (bitmap != null) { + BitmapTexture texture = new BitmapTexture(bitmap); + texture.setThrottled(true); + updateContent(texture); + if (mListener != null && isActiveSlot) { + mListener.onContentInvalidated(); + } + } + } + + private void updateContent(Texture content) { + mContent = content; + + int width = mContent.getWidth(); + int height = mContent.getHeight(); + + float scalex = mDisplayItemSize / (float) width; + float scaley = mDisplayItemSize / (float) height; + float scale = Math.min(scalex, scaley); + + width = (int) Math.floor(width * scale); + height = (int) Math.floor(height * scale); + + setSize(width, height); + } + + @Override + public boolean render(GLCanvas canvas, int pass) { + if (pass == 0) { + Path path = null; + if (mMediaItem != null) path = mMediaItem.getPath(); + mSelectionDrawer.draw(canvas, mContent, mWidth, mHeight, + getRotation(), path, mMediaType); + return (mFocusIndex == mSlotIndex); + } else if (pass == 1) { + mSelectionDrawer.drawFocus(canvas, mWidth, mHeight); + } + return false; + } + + @Override + public void startLoadBitmap() { + if (mDisplayItemSize < MIN_THUMB_SIZE) { + Path path = mMediaItem.getPath(); + if (mImageCache.containsKey(path)) { + Bitmap bitmap = mImageCache.get(path); + updateImage(bitmap, false); + return; + } + mFuture = mThreadPool.submit(this, this); + } else { + mFuture = mThreadPool.submit(mMediaItem.requestImage( + MediaItem.TYPE_MICROTHUMBNAIL), this); + } + } + + // This gets the bitmap and scale it down. + public Bitmap run(JobContext jc) { + Job job = mMediaItem.requestImage( + MediaItem.TYPE_MICROTHUMBNAIL); + Bitmap bitmap = job.run(jc); + if (bitmap != null) { + bitmap = BitmapUtils.resizeDownBySideLength( + bitmap, mDisplayItemSize, true); + } + return bitmap; + } + + @Override + public void cancelLoadBitmap() { + if (mFuture != null) { + mFuture.cancel(); + } + } + + @Override + public void onFutureDone(Future bitmap) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_LOAD_BITMAP_DONE, this)); + } + + private void onLoadBitmapDone() { + Future future = mFuture; + mFuture = null; + Bitmap bitmap = future.get(); + boolean isCancelled = future.isCancelled(); + if (mDisplayItemSize < MIN_THUMB_SIZE && (bitmap != null || !isCancelled)) { + Path path = mMediaItem.getPath(); + mImageCache.put(path, bitmap); + } + updateImage(bitmap, isCancelled); + } + + @Override + public String toString() { + return String.format("AlbumDisplayItem[%s]", mSlotIndex); + } + } + + public void onSizeChanged(int size) { + if (mSize != size) { + mSize = size; + if (mListener != null) mListener.onSizeChanged(mSize); + } + } + + public void onWindowContentChanged(int index) { + if (index >= mContentStart && index < mContentEnd && mIsActive) { + updateSlotContent(index); + } + } + + public void resume() { + mIsActive = true; + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + prepareSlotContent(i); + } + updateAllImageRequests(); + } + + public void pause() { + mIsActive = false; + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mImageCache.clear(); + } +} diff --git a/src/com/android/gallery3d/ui/AlbumView.java b/src/com/android/gallery3d/ui/AlbumView.java new file mode 100644 index 000000000..417611a69 --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumView.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.ui.PositionRepository.Position; + +import android.graphics.Rect; + +public class AlbumView extends SlotView { + @SuppressWarnings("unused") + private static final String TAG = "AlbumView"; + private static final int CACHE_SIZE = 64; + + private int mVisibleStart = 0; + private int mVisibleEnd = 0; + + private AlbumSlidingWindow mDataWindow; + private final GalleryActivity mActivity; + private SelectionDrawer mSelectionDrawer; + private int mSlotWidth, mSlotHeight; + private int mDisplayItemSize; + + private boolean mIsActive = false; + + public static interface Model { + public int size(); + public MediaItem get(int index); + public void setActiveWindow(int start, int end); + public void setModelListener(ModelListener listener); + } + + public static interface ModelListener { + public void onWindowContentChanged(int index); + public void onSizeChanged(int size); + } + + public AlbumView(GalleryActivity activity, + int slotWidth, int slotHeight, int displayItemSize) { + super(activity.getAndroidContext()); + mSlotWidth = slotWidth; + mSlotHeight = slotHeight; + mDisplayItemSize = displayItemSize; + setSlotSize(slotWidth, slotHeight); + mActivity = activity; + } + + public void setSelectionDrawer(SelectionDrawer drawer) { + mSelectionDrawer = drawer; + if (mDataWindow != null) mDataWindow.setSelectionDrawer(drawer); + } + + public void setModel(Model model) { + if (mDataWindow != null) { + mDataWindow.setListener(null); + setSlotCount(0); + mDataWindow = null; + } + if (model != null) { + mDataWindow = new AlbumSlidingWindow( + mActivity, model, CACHE_SIZE, + mSlotWidth, mSlotHeight, mDisplayItemSize); + mDataWindow.setSelectionDrawer(mSelectionDrawer); + mDataWindow.setListener(new MyDataModelListener()); + setSlotCount(model.size()); + updateVisibleRange(getVisibleStart(), getVisibleEnd()); + } + } + + public void setFocusIndex(int slotIndex) { + if (mDataWindow != null) { + mDataWindow.setFocusIndex(slotIndex); + } + } + + private void putSlotContent(int slotIndex, DisplayItem item) { + Rect rect = getSlotRect(slotIndex); + Position position = new Position( + (rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2, 0); + putDisplayItem(position, position, item); + } + + private void updateVisibleRange(int start, int end) { + if (start == mVisibleStart && end == mVisibleEnd) { + // we need to set the mDataWindow active range in any case. + mDataWindow.setActiveWindow(start, end); + return; + } + + if (!mIsActive) { + mVisibleStart = start; + mVisibleEnd = end; + mDataWindow.setActiveWindow(start, end); + return; + } + + if (start >= mVisibleEnd || mVisibleStart >= end) { + for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) { + DisplayItem item = mDataWindow.get(i); + if (item != null) removeDisplayItem(item); + } + mDataWindow.setActiveWindow(start, end); + for (int i = start; i < end; ++i) { + putSlotContent(i, mDataWindow.get(i)); + } + } else { + for (int i = mVisibleStart; i < start; ++i) { + DisplayItem item = mDataWindow.get(i); + if (item != null) removeDisplayItem(item); + } + for (int i = end, n = mVisibleEnd; i < n; ++i) { + DisplayItem item = mDataWindow.get(i); + if (item != null) removeDisplayItem(item); + } + mDataWindow.setActiveWindow(start, end); + for (int i = start, n = mVisibleStart; i < n; ++i) { + putSlotContent(i, mDataWindow.get(i)); + } + for (int i = mVisibleEnd; i < end; ++i) { + putSlotContent(i, mDataWindow.get(i)); + } + } + + mVisibleStart = start; + mVisibleEnd = end; + } + + @Override + protected void onLayoutChanged(int width, int height) { + // Reput all the items + updateVisibleRange(0, 0); + updateVisibleRange(getVisibleStart(), getVisibleEnd()); + } + + @Override + protected void onScrollPositionChanged(int position) { + super.onScrollPositionChanged(position); + updateVisibleRange(getVisibleStart(), getVisibleEnd()); + } + + @Override + protected void render(GLCanvas canvas) { + mSelectionDrawer.prepareDrawing(); + super.render(canvas); + } + + private class MyDataModelListener implements AlbumSlidingWindow.Listener { + + public void onContentInvalidated() { + invalidate(); + } + + public void onSizeChanged(int size) { + // If the layout parameters are changed, we need reput all items. + if (setSlotCount(size)) updateVisibleRange(0, 0); + updateVisibleRange(getVisibleStart(), getVisibleEnd()); + invalidate(); + } + + public void onWindowContentChanged( + int slotIndex, DisplayItem old, DisplayItem update) { + removeDisplayItem(old); + putSlotContent(slotIndex, update); + } + } + + public void resume() { + mIsActive = true; + mDataWindow.resume(); + for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) { + putSlotContent(i, mDataWindow.get(i)); + } + } + + public void pause() { + mIsActive = false; + for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) { + removeDisplayItem(mDataWindow.get(i)); + } + mDataWindow.pause(); + } +} diff --git a/src/com/android/gallery3d/ui/BasicTexture.java b/src/com/android/gallery3d/ui/BasicTexture.java new file mode 100644 index 000000000..e93006326 --- /dev/null +++ b/src/com/android/gallery3d/ui/BasicTexture.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.common.Utils; + +import java.lang.ref.WeakReference; +import java.util.WeakHashMap; + +// BasicTexture is a Texture corresponds to a real GL texture. +// The state of a BasicTexture indicates whether its data is loaded to GL memory. +// If a BasicTexture is loaded into GL memory, it has a GL texture id. +abstract class BasicTexture implements Texture { + + @SuppressWarnings("unused") + private static final String TAG = "BasicTexture"; + protected static final int UNSPECIFIED = -1; + + protected static final int STATE_UNLOADED = 0; + protected static final int STATE_LOADED = 1; + protected static final int STATE_ERROR = -1; + + protected int mId; + protected int mState; + + protected int mWidth = UNSPECIFIED; + protected int mHeight = UNSPECIFIED; + + private int mTextureWidth; + private int mTextureHeight; + + protected WeakReference mCanvasRef = null; + private static WeakHashMap sAllTextures + = new WeakHashMap(); + private static ThreadLocal sInFinalizer = new ThreadLocal(); + + protected BasicTexture(GLCanvas canvas, int id, int state) { + setAssociatedCanvas(canvas); + mId = id; + mState = state; + synchronized (sAllTextures) { + sAllTextures.put(this, null); + } + } + + protected BasicTexture() { + this(null, 0, STATE_UNLOADED); + } + + protected void setAssociatedCanvas(GLCanvas canvas) { + mCanvasRef = canvas == null + ? null + : new WeakReference(canvas); + } + + /** + * Sets the content size of this texture. In OpenGL, the actual texture + * size must be of power of 2, the size of the content may be smaller. + */ + protected void setSize(int width, int height) { + mWidth = width; + mHeight = height; + mTextureWidth = Utils.nextPowerOf2(width); + mTextureHeight = Utils.nextPowerOf2(height); + } + + public int getId() { + return mId; + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + // Returns the width rounded to the next power of 2. + public int getTextureWidth() { + return mTextureWidth; + } + + // Returns the height rounded to the next power of 2. + public int getTextureHeight() { + return mTextureHeight; + } + + public void draw(GLCanvas canvas, int x, int y) { + canvas.drawTexture(this, x, y, getWidth(), getHeight()); + } + + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + canvas.drawTexture(this, x, y, w, h); + } + + // onBind is called before GLCanvas binds this texture. + // It should make sure the data is uploaded to GL memory. + abstract protected boolean onBind(GLCanvas canvas); + + public boolean isLoaded(GLCanvas canvas) { + return mState == STATE_LOADED && mCanvasRef.get() == canvas; + } + + // recycle() is called when the texture will never be used again, + // so it can free all resources. + public void recycle() { + freeResource(); + } + + // yield() is called when the texture will not be used temporarily, + // so it can free some resources. + // The default implementation unloads the texture from GL memory, so + // the subclass should make sure it can reload the texture to GL memory + // later, or it will have to override this method. + public void yield() { + freeResource(); + } + + private void freeResource() { + GLCanvas canvas = mCanvasRef == null ? null : mCanvasRef.get(); + if (canvas != null && isLoaded(canvas)) { + canvas.unloadTexture(this); + } + mState = BasicTexture.STATE_UNLOADED; + setAssociatedCanvas(null); + } + + @Override + protected void finalize() { + sInFinalizer.set(BasicTexture.class); + recycle(); + sInFinalizer.set(null); + } + + // This is for deciding if we can call Bitmap's recycle(). + // We cannot call Bitmap's recycle() in finalizer because at that point + // the finalizer of Bitmap may already be called so recycle() will crash. + public static boolean inFinalizer() { + return sInFinalizer.get() != null; + } + + public static void yieldAllTextures() { + synchronized (sAllTextures) { + for (BasicTexture t : sAllTextures.keySet()) { + t.yield(); + } + } + } +} diff --git a/src/com/android/gallery3d/ui/BitmapTexture.java b/src/com/android/gallery3d/ui/BitmapTexture.java new file mode 100644 index 000000000..046bda94c --- /dev/null +++ b/src/com/android/gallery3d/ui/BitmapTexture.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.common.Utils; + +import android.graphics.Bitmap; + +// BitmapTexture is a texture whose content is specified by a fixed Bitmap. +// +// The texture does not own the Bitmap. The user should make sure the Bitmap +// is valid during the texture's lifetime. When the texture is recycled, it +// does not free the Bitmap. +public class BitmapTexture extends UploadedTexture { + protected Bitmap mContentBitmap; + + public BitmapTexture(Bitmap bitmap) { + Utils.assertTrue(bitmap != null && !bitmap.isRecycled()); + mContentBitmap = bitmap; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + // Do nothing. + } + + @Override + protected Bitmap onGetBitmap() { + return mContentBitmap; + } + + public Bitmap getBitmap() { + return mContentBitmap; + } +} diff --git a/src/com/android/gallery3d/ui/BitmapTileProvider.java b/src/com/android/gallery3d/ui/BitmapTileProvider.java new file mode 100644 index 000000000..a47337fa2 --- /dev/null +++ b/src/com/android/gallery3d/ui/BitmapTileProvider.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.common.BitmapUtils; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Bitmap.Config; + +import java.util.ArrayList; + +public class BitmapTileProvider implements TileImageView.Model { + private final Bitmap mBackup; + private final Bitmap[] mMipmaps; + private final Config mConfig; + private final int mImageWidth; + private final int mImageHeight; + + private boolean mRecycled = false; + + public BitmapTileProvider(Bitmap bitmap, int maxBackupSize) { + mImageWidth = bitmap.getWidth(); + mImageHeight = bitmap.getHeight(); + ArrayList list = new ArrayList(); + list.add(bitmap); + while (bitmap.getWidth() > maxBackupSize + || bitmap.getHeight() > maxBackupSize) { + bitmap = BitmapUtils.resizeBitmapByScale(bitmap, 0.5f, false); + list.add(bitmap); + } + + mBackup = list.remove(list.size() - 1); + mMipmaps = list.toArray(new Bitmap[list.size()]); + mConfig = Config.ARGB_8888; + } + + public Bitmap getBackupImage() { + return mBackup; + } + + public int getImageHeight() { + return mImageHeight; + } + + public int getImageWidth() { + return mImageWidth; + } + + public int getLevelCount() { + return mMipmaps.length; + } + + public Bitmap getTile(int level, int x, int y, int tileSize) { + Bitmap result = Bitmap.createBitmap(tileSize, tileSize, mConfig); + Canvas canvas = new Canvas(result); + canvas.drawBitmap(mMipmaps[level], -(x >> level), -(y >> level), null); + return result; + } + + public void recycle() { + if (mRecycled) return; + mRecycled = true; + for (Bitmap bitmap : mMipmaps) { + BitmapUtils.recycleSilently(bitmap); + } + BitmapUtils.recycleSilently(mBackup); + } + + public int getRotation() { + return 0; + } + + public boolean isFailedToLoad() { + return false; + } +} diff --git a/src/com/android/gallery3d/ui/BoxBlurFilter.java b/src/com/android/gallery3d/ui/BoxBlurFilter.java new file mode 100644 index 000000000..0497a61fa --- /dev/null +++ b/src/com/android/gallery3d/ui/BoxBlurFilter.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Bitmap; + + +public class BoxBlurFilter { + private static final int RED_MASK = 0xff0000; + private static final int RED_MASK_SHIFT = 16; + private static final int GREEN_MASK = 0x00ff00; + private static final int GREEN_MASK_SHIFT = 8; + private static final int BLUE_MASK = 0x0000ff; + private static final int RADIUS = 4; + private static final int KERNEL_SIZE = RADIUS * 2 + 1; + private static final int NUM_COLORS = 256; + private static final int[] KERNEL_NORM = new int[KERNEL_SIZE * NUM_COLORS]; + + public static final int MODE_REPEAT = 1; + public static final int MODE_CLAMP = 2; + + static { + int index = 0; + // Build a lookup table from summed to normalized kernel values. + // The formula: KERNAL_NORM[value] = value / KERNEL_SIZE + for (int i = 0; i < NUM_COLORS; ++i) { + for (int j = 0; j < KERNEL_SIZE; ++j) { + KERNEL_NORM[index++] = i; + } + } + } + + private BoxBlurFilter() { + } + + private static int sample(int x, int width, int mode) { + if (x >= 0 && x < width) return x; + return mode == MODE_REPEAT + ? x < 0 ? x + width : x - width + : x < 0 ? 0 : width - 1; + } + + public static void apply( + Bitmap bitmap, int horizontalMode, int verticalMode) { + + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + int data[] = new int[width * height]; + bitmap.getPixels(data, 0, width, 0, 0, width, height); + int temp[] = new int[width * height]; + applyOneDimension(data, temp, width, height, horizontalMode); + applyOneDimension(temp, data, height, width, verticalMode); + bitmap.setPixels(data, 0, width, 0, 0, width, height); + } + + private static void applyOneDimension( + int[] in, int[] out, int width, int height, int mode) { + for (int y = 0, read = 0; y < height; ++y, read += width) { + // Evaluate the kernel for the first pixel in the row. + int red = 0; + int green = 0; + int blue = 0; + for (int i = -RADIUS; i <= RADIUS; ++i) { + int argb = in[read + sample(i, width, mode)]; + red += (argb & RED_MASK) >> RED_MASK_SHIFT; + green += (argb & GREEN_MASK) >> GREEN_MASK_SHIFT; + blue += argb & BLUE_MASK; + } + for (int x = 0, write = y; x < width; ++x, write += height) { + // Output the current pixel. + out[write] = 0xFF000000 + | (KERNEL_NORM[red] << RED_MASK_SHIFT) + | (KERNEL_NORM[green] << GREEN_MASK_SHIFT) + | KERNEL_NORM[blue]; + + // Slide to the next pixel, adding the new rightmost pixel and + // subtracting the former leftmost. + int prev = in[read + sample(x - RADIUS, width, mode)]; + int next = in[read + sample(x + RADIUS + 1, width, mode)]; + red += ((next & RED_MASK) - (prev & RED_MASK)) >> RED_MASK_SHIFT; + green += ((next & GREEN_MASK) - (prev & GREEN_MASK)) >> GREEN_MASK_SHIFT; + blue += (next & BLUE_MASK) - (prev & BLUE_MASK); + } + } + } +} diff --git a/src/com/android/gallery3d/ui/CacheBarView.java b/src/com/android/gallery3d/ui/CacheBarView.java new file mode 100644 index 000000000..40f84d8f9 --- /dev/null +++ b/src/com/android/gallery3d/ui/CacheBarView.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Message; +import android.os.StatFs; +import android.text.format.Formatter; +import android.view.View.MeasureSpec; + +import java.io.File; + +public class CacheBarView extends GLView implements TextButton.OnClickedListener { + private static final String TAG = "CacheBarView"; + private static final int FONT_COLOR = 0xffffffff; + private static final int MSG_REFRESH_STORAGE = 1; + private static final int PIN_SIZE = 36; + + public interface Listener { + void onDoneClicked(); + } + + private GalleryActivity mActivity; + private Context mContext; + + private StorageInfo mStorageInfo; + private long mUserChangeDelta; + private Future mStorageInfoFuture; + private Handler mHandler; + + private int mTotalHeight; + private int mPinLeftMargin; + private int mPinRightMargin; + private int mButtonRightMargin; + + private NinePatchTexture mBackground; + private GLView mLeftPin; // The pin icon. + private GLView mLeftLabel; // "Make available offline" + private ProgressBar mStorageBar; + private Label mStorageLabel; // "27.26 GB free" + private TextButton mDoneButton; // "Done" + + private Listener mListener; + + public CacheBarView(GalleryActivity activity, int resBackground, int height, + int pinLeftMargin, int pinRightMargin, int buttonRightMargin, + int fontSize) { + mActivity = activity; + mContext = activity.getAndroidContext(); + + mPinLeftMargin = pinLeftMargin; + mPinRightMargin = pinRightMargin; + mButtonRightMargin = buttonRightMargin; + + mBackground = new NinePatchTexture(mContext, resBackground); + Rect paddings = mBackground.getPaddings(); + + // The total height of the strip that includes the bar containing Pin, + // Label, DoneButton, ..., ect. and the extended fading bar. + mTotalHeight = height + paddings.top; + + mLeftPin = new Icon(mContext, R.drawable.ic_manage_pin, PIN_SIZE, PIN_SIZE); + mLeftLabel = new Label(mContext, R.string.make_available_offline, + fontSize, FONT_COLOR); + addComponent(mLeftPin); + addComponent(mLeftLabel); + + mDoneButton = new TextButton(mContext, R.string.done); + mDoneButton.setOnClickListener(this); + NinePatchTexture normal = new NinePatchTexture( + mContext, R.drawable.btn_default_normal_holo_dark); + NinePatchTexture pressed = new NinePatchTexture( + mContext, R.drawable.btn_default_pressed_holo_dark); + mDoneButton.setNormalBackground(normal); + mDoneButton.setPressedBackground(pressed); + addComponent(mDoneButton); + + // Initially the progress bar and label are invisible. + // It will be made visible after we have the storage information. + mStorageBar = new ProgressBar(mContext, + R.drawable.progress_primary_holo_dark, + R.drawable.progress_secondary_holo_dark, + R.drawable.progress_bg_holo_dark); + mStorageLabel = new Label(mContext, "", 14, Color.WHITE); + addComponent(mStorageBar); + addComponent(mStorageLabel); + mStorageBar.setVisibility(GLView.INVISIBLE); + mStorageLabel.setVisibility(GLView.INVISIBLE); + + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case MSG_REFRESH_STORAGE: + mStorageInfo = (StorageInfo) msg.obj; + refreshStorageInfo(); + break; + } + } + }; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + // Called by mDoneButton + public void onClicked(GLView source) { + if (mListener != null) { + mListener.onDoneClicked(); + } + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + // The size of mStorageLabel can change, so we need to layout + // even if the size of CacheBarView does not change. + int w = right - left; + int h = bottom - top; + + mLeftPin.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int pinH = mLeftPin.getMeasuredHeight(); + int pinW = mLeftPin.getMeasuredWidth(); + int pinT = (h - pinH) / 2; + int pinL = mPinLeftMargin; + mLeftPin.layout(pinL, pinT, pinL + pinW, pinT + pinH); + + mLeftLabel.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int labelH = mLeftLabel.getMeasuredHeight(); + int labelW = mLeftLabel.getMeasuredWidth(); + int labelT = (h - labelH) / 2; + int labelL = pinL + pinW + mPinRightMargin; + mLeftLabel.layout(labelL, labelT, labelL + labelW, labelT + labelH); + + mDoneButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int doneH = mDoneButton.getMeasuredHeight(); + int doneW = mDoneButton.getMeasuredWidth(); + int doneT = (h - doneH) / 2; + int doneR = w - mButtonRightMargin; + mDoneButton.layout(doneR - doneW, doneT, doneR, doneT + doneH); + + int centerX = w / 2; + int centerY = h / 2; + + int capBarH = 20; + int capBarW = 200; + int capBarT = centerY - capBarH / 2; + int capBarL = centerX - capBarW / 2; + mStorageBar.layout(capBarL, capBarT, capBarL + capBarW, + capBarT + capBarH); + + mStorageLabel.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int capLabelH = mStorageLabel.getMeasuredHeight(); + int capLabelW = mStorageLabel.getMeasuredWidth(); + int capLabelT = centerY - capLabelH / 2; + int capLabelL = centerX + capBarW / 2 + 8; + mStorageLabel.layout(capLabelL , capLabelT, capLabelL + capLabelW, + capLabelT + capLabelH); + } + + public void refreshStorageInfo() { + long used = mStorageInfo.usedBytes; + long total = mStorageInfo.totalBytes; + long cached = mStorageInfo.usedCacheBytes; + long target = mStorageInfo.targetCacheBytes; + + double primary = (double) used / total; + double secondary = + (double) (used - cached + target + mUserChangeDelta) / total; + + mStorageBar.setProgress((int) (primary * 10000)); + mStorageBar.setSecondaryProgress((int) (secondary * 10000)); + + long freeBytes = mStorageInfo.totalBytes - mStorageInfo.usedBytes; + String sizeString = Formatter.formatFileSize(mContext, freeBytes); + String label = mContext.getString(R.string.free_space_format, sizeString); + mStorageLabel.setText(label); + mStorageBar.setVisibility(GLView.VISIBLE); + mStorageLabel.setVisibility(GLView.VISIBLE); + requestLayout(); // because the size of the label may have changed. + } + + public void increaseTargetCacheSize(long delta) { + mUserChangeDelta += delta; + refreshStorageInfo(); + } + + @Override + protected void renderBackground(GLCanvas canvas) { + Rect paddings = mBackground.getPaddings(); + mBackground.draw(canvas, 0, -paddings.top, getWidth(), mTotalHeight); + } + + public void resume() { + mStorageInfoFuture = mActivity.getThreadPool().submit( + new StorageInfoJob(), + new FutureListener() { + public void onFutureDone(Future future) { + mStorageInfoFuture = null; + if (!future.isCancelled()) { + mHandler.sendMessage(mHandler.obtainMessage( + MSG_REFRESH_STORAGE, future.get())); + } + } + }); + } + + public void pause() { + if (mStorageInfoFuture != null) { + mStorageInfoFuture.cancel(); + mStorageInfoFuture = null; + } + mStorageBar.setVisibility(GLView.INVISIBLE); + mStorageLabel.setVisibility(GLView.INVISIBLE); + } + + public static class StorageInfo { + long totalBytes; // number of bytes the storage has. + long usedBytes; // number of bytes already used. + long usedCacheBytes; // number of bytes used for the cache (should be less + // then usedBytes). + long targetCacheBytes;// number of bytes used for the cache + // if all pending downloads (and removals) are completed. + } + + private class StorageInfoJob implements Job { + public StorageInfo run(JobContext jc) { + File cacheDir = mContext.getExternalCacheDir(); + if (cacheDir == null) { + cacheDir = mContext.getCacheDir(); + } + String path = cacheDir.getAbsolutePath(); + StatFs stat = new StatFs(path); + long blockSize = stat.getBlockSize(); + long availableBlocks = stat.getAvailableBlocks(); + long totalBlocks = stat.getBlockCount(); + StorageInfo si = new StorageInfo(); + si.totalBytes = blockSize * totalBlocks; + si.usedBytes = blockSize * (totalBlocks - availableBlocks); + si.usedCacheBytes = mActivity.getDataManager().getTotalUsedCacheSize(); + si.targetCacheBytes = mActivity.getDataManager().getTotalTargetCacheSize(); + return si; + } + } +} diff --git a/src/com/android/gallery3d/ui/CanvasTexture.java b/src/com/android/gallery3d/ui/CanvasTexture.java new file mode 100644 index 000000000..679a4bcdc --- /dev/null +++ b/src/com/android/gallery3d/ui/CanvasTexture.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Bitmap.Config; + +// CanvasTexture is a texture whose content is the drawing on a Canvas. +// The subclasses should override onDraw() to draw on the bitmap. +// By default CanvasTexture is not opaque. +abstract class CanvasTexture extends UploadedTexture { + protected Canvas mCanvas; + private final Config mConfig; + + public CanvasTexture(int width, int height) { + mConfig = Config.ARGB_8888; + setSize(width, height); + setOpaque(false); + } + + @Override + protected Bitmap onGetBitmap() { + Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, mConfig); + mCanvas = new Canvas(bitmap); + onDraw(mCanvas, bitmap); + return bitmap; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + if (!inFinalizer()) { + bitmap.recycle(); + } + } + + abstract protected void onDraw(Canvas canvas, Bitmap backing); +} diff --git a/src/com/android/gallery3d/ui/ColorTexture.java b/src/com/android/gallery3d/ui/ColorTexture.java new file mode 100644 index 000000000..24e8914b5 --- /dev/null +++ b/src/com/android/gallery3d/ui/ColorTexture.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.common.Utils; + +// ColorTexture is a texture which fills the rectangle with the specified color. +public class ColorTexture implements Texture { + + private final int mColor; + private int mWidth; + private int mHeight; + + public ColorTexture(int color) { + mColor = color; + mWidth = 1; + mHeight = 1; + } + + public void draw(GLCanvas canvas, int x, int y) { + draw(canvas, x, y, mWidth, mHeight); + } + + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + canvas.fillRect(x, y, w, h, mColor); + } + + public boolean isOpaque() { + return Utils.isOpaque(mColor); + } + + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } +} diff --git a/src/com/android/gallery3d/ui/Config.java b/src/com/android/gallery3d/ui/Config.java new file mode 100644 index 000000000..5c5b6210a --- /dev/null +++ b/src/com/android/gallery3d/ui/Config.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +interface DetailsWindowConfig { + public static final int FONT_SIZE = 18; + public static final int PREFERRED_WIDTH = 400; + public static final int LEFT_RIGHT_EXTRA_PADDING = 9; + public static final int TOP_BOTTOM_EXTRA_PADDING = 9; + public static final int LINE_SPACING = 5; + public static final int FIRST_LINE_SPACING = 18; +} + +interface TextButtonConfig { + public static final int HORIZONTAL_PADDINGS = 16; + public static final int VERTICAL_PADDINGS = 5; +} diff --git a/src/com/android/gallery3d/ui/CropView.java b/src/com/android/gallery3d/ui/CropView.java new file mode 100644 index 000000000..9c59c9a84 --- /dev/null +++ b/src/com/android/gallery3d/ui/CropView.java @@ -0,0 +1,801 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.anim.Animation; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.RectF; +import android.media.FaceDetector; +import android.os.Handler; +import android.os.Message; +import android.view.MotionEvent; +import android.view.animation.DecelerateInterpolator; +import android.widget.Toast; + +import java.util.ArrayList; +import javax.microedition.khronos.opengles.GL11; + +/** + * The activity can crop specific region of interest from an image. + */ +public class CropView extends GLView { + private static final String TAG = "CropView"; + + private static final int FACE_PIXEL_COUNT = 120000; // around 400x300 + + private static final int COLOR_OUTLINE = 0xFF008AFF; + private static final int COLOR_FACE_OUTLINE = 0xFF000000; + + private static final float OUTLINE_WIDTH = 3f; + + private static final int SIZE_UNKNOWN = -1; + private static final int TOUCH_TOLERANCE = 30; + + private static final float MIN_SELECTION_LENGTH = 16f; + public static final float UNSPECIFIED = -1f; + + private static final int MAX_FACE_COUNT = 3; + private static final float FACE_EYE_RATIO = 2f; + + private static final int ANIMATION_DURATION = 1250; + + private static final int MOVE_LEFT = 1; + private static final int MOVE_TOP = 2; + private static final int MOVE_RIGHT = 4; + private static final int MOVE_BOTTOM = 8; + private static final int MOVE_BLOCK = 16; + + private static final float MAX_SELECTION_RATIO = 0.8f; + private static final float MIN_SELECTION_RATIO = 0.4f; + private static final float SELECTION_RATIO = 0.60f; + private static final int ANIMATION_TRIGGER = 64; + + private static final int MSG_UPDATE_FACES = 1; + + private float mAspectRatio = UNSPECIFIED; + private float mSpotlightRatioX = 0; + private float mSpotlightRatioY = 0; + + private Handler mMainHandler; + + private FaceHighlightView mFaceDetectionView; + private HighlightRectangle mHighlightRectangle; + private TileImageView mImageView; + private AnimationController mAnimation = new AnimationController(); + + private int mImageWidth = SIZE_UNKNOWN; + private int mImageHeight = SIZE_UNKNOWN; + + private GalleryActivity mActivity; + + private GLPaint mPaint = new GLPaint(); + private GLPaint mFacePaint = new GLPaint(); + + private int mImageRotation; + + public CropView(GalleryActivity activity) { + mActivity = activity; + mImageView = new TileImageView(activity); + mFaceDetectionView = new FaceHighlightView(); + mHighlightRectangle = new HighlightRectangle(); + + addComponent(mImageView); + addComponent(mFaceDetectionView); + addComponent(mHighlightRectangle); + + mHighlightRectangle.setVisibility(GLView.INVISIBLE); + + mPaint.setColor(COLOR_OUTLINE); + mPaint.setLineWidth(OUTLINE_WIDTH); + + mFacePaint.setColor(COLOR_FACE_OUTLINE); + mFacePaint.setLineWidth(OUTLINE_WIDTH); + + mMainHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + Utils.assertTrue(message.what == MSG_UPDATE_FACES); + ((DetectFaceTask) message.obj).updateFaces(); + } + }; + } + + public void setAspectRatio(float ratio) { + mAspectRatio = ratio; + } + + public void setSpotlightRatio(float ratioX, float ratioY) { + mSpotlightRatioX = ratioX; + mSpotlightRatioY = ratioY; + } + + @Override + public void onLayout(boolean changed, int l, int t, int r, int b) { + int width = r - l; + int height = b - t; + + mFaceDetectionView.layout(0, 0, width, height); + mHighlightRectangle.layout(0, 0, width, height); + mImageView.layout(0, 0, width, height); + if (mImageHeight != SIZE_UNKNOWN) { + mAnimation.initialize(); + if (mHighlightRectangle.getVisibility() == GLView.VISIBLE) { + mAnimation.parkNow( + mHighlightRectangle.mHighlightRect); + } + } + } + + private boolean setImageViewPosition(int centerX, int centerY, float scale) { + int inverseX = mImageWidth - centerX; + int inverseY = mImageHeight - centerY; + TileImageView t = mImageView; + int rotation = mImageRotation; + switch (rotation) { + case 0: return t.setPosition(centerX, centerY, scale, 0); + case 90: return t.setPosition(centerY, inverseX, scale, 90); + case 180: return t.setPosition(inverseX, inverseY, scale, 180); + case 270: return t.setPosition(inverseY, centerX, scale, 270); + default: throw new IllegalArgumentException(String.valueOf(rotation)); + } + } + + @Override + public void render(GLCanvas canvas) { + AnimationController a = mAnimation; + if (a.calculate(canvas.currentAnimationTimeMillis())) invalidate(); + setImageViewPosition(a.getCenterX(), a.getCenterY(), a.getScale()); + super.render(canvas); + } + + @Override + public void renderBackground(GLCanvas canvas) { + canvas.clearBuffer(); + } + + public RectF getCropRectangle() { + if (mHighlightRectangle.getVisibility() == GLView.INVISIBLE) return null; + RectF rect = mHighlightRectangle.mHighlightRect; + RectF result = new RectF(rect.left * mImageWidth, rect.top * mImageHeight, + rect.right * mImageWidth, rect.bottom * mImageHeight); + return result; + } + + public int getImageWidth() { + return mImageWidth; + } + + public int getImageHeight() { + return mImageHeight; + } + + private class FaceHighlightView extends GLView { + private static final int INDEX_NONE = -1; + private ArrayList mFaces = new ArrayList(); + private RectF mRect = new RectF(); + private int mPressedFaceIndex = INDEX_NONE; + + public void addFace(RectF faceRect) { + mFaces.add(faceRect); + invalidate(); + } + + private void renderFace(GLCanvas canvas, RectF face, boolean pressed) { + GL11 gl = canvas.getGLInstance(); + if (pressed) { + gl.glEnable(GL11.GL_STENCIL_TEST); + gl.glClear(GL11.GL_STENCIL_BUFFER_BIT); + gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_REPLACE); + gl.glStencilFunc(GL11.GL_ALWAYS, 1, 1); + } + + RectF r = mAnimation.mapRect(face, mRect); + canvas.fillRect(r.left, r.top, r.width(), r.height(), Color.TRANSPARENT); + canvas.drawRect(r.left, r.top, r.width(), r.height(), mFacePaint); + + if (pressed) { + gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP); + } + } + + @Override + protected void renderBackground(GLCanvas canvas) { + ArrayList faces = mFaces; + for (int i = 0, n = faces.size(); i < n; ++i) { + renderFace(canvas, faces.get(i), i == mPressedFaceIndex); + } + + GL11 gl = canvas.getGLInstance(); + if (mPressedFaceIndex != INDEX_NONE) { + gl.glStencilFunc(GL11.GL_NOTEQUAL, 1, 1); + canvas.fillRect(0, 0, getWidth(), getHeight(), 0x66000000); + gl.glDisable(GL11.GL_STENCIL_TEST); + } + } + + private void setPressedFace(int index) { + if (mPressedFaceIndex == index) return; + mPressedFaceIndex = index; + invalidate(); + } + + private int getFaceIndexByPosition(float x, float y) { + ArrayList faces = mFaces; + for (int i = 0, n = faces.size(); i < n; ++i) { + RectF r = mAnimation.mapRect(faces.get(i), mRect); + if (r.contains(x, y)) return i; + } + return INDEX_NONE; + } + + @Override + protected boolean onTouch(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: { + setPressedFace(getFaceIndexByPosition(x, y)); + break; + } + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + int index = mPressedFaceIndex; + setPressedFace(INDEX_NONE); + if (index != INDEX_NONE) { + mHighlightRectangle.setRectangle(mFaces.get(index)); + mHighlightRectangle.setVisibility(GLView.VISIBLE); + setVisibility(GLView.INVISIBLE); + } + } + } + return true; + } + } + + private class AnimationController extends Animation { + private int mCurrentX; + private int mCurrentY; + private float mCurrentScale; + private int mStartX; + private int mStartY; + private float mStartScale; + private int mTargetX; + private int mTargetY; + private float mTargetScale; + + public AnimationController() { + setDuration(ANIMATION_DURATION); + setInterpolator(new DecelerateInterpolator(4)); + } + + public void initialize() { + mCurrentX = mImageWidth / 2; + mCurrentY = mImageHeight / 2; + mCurrentScale = Math.min(2, Math.min( + (float) getWidth() / mImageWidth, + (float) getHeight() / mImageHeight)); + } + + public void startParkingAnimation(RectF highlight) { + RectF r = mAnimation.mapRect(highlight, new RectF()); + int width = getWidth(); + int height = getHeight(); + + float wr = r.width() / width; + float hr = r.height() / height; + final int d = ANIMATION_TRIGGER; + if (wr >= MIN_SELECTION_RATIO && wr < MAX_SELECTION_RATIO + && hr >= MIN_SELECTION_RATIO && hr < MAX_SELECTION_RATIO + && r.left >= d && r.right < width - d + && r.top >= d && r.bottom < height - d) return; + + mStartX = mCurrentX; + mStartY = mCurrentY; + mStartScale = mCurrentScale; + calculateTarget(highlight); + start(); + } + + public void parkNow(RectF highlight) { + calculateTarget(highlight); + forceStop(); + mStartX = mCurrentX = mTargetX; + mStartY = mCurrentY = mTargetY; + mStartScale = mCurrentScale = mTargetScale; + } + + public void inverseMapPoint(PointF point) { + float s = mCurrentScale; + point.x = Utils.clamp(((point.x - getWidth() * 0.5f) / s + + mCurrentX) / mImageWidth, 0, 1); + point.y = Utils.clamp(((point.y - getHeight() * 0.5f) / s + + mCurrentY) / mImageHeight, 0, 1); + } + + public RectF mapRect(RectF input, RectF output) { + float offsetX = getWidth() * 0.5f; + float offsetY = getHeight() * 0.5f; + int x = mCurrentX; + int y = mCurrentY; + float s = mCurrentScale; + output.set( + offsetX + (input.left * mImageWidth - x) * s, + offsetY + (input.top * mImageHeight - y) * s, + offsetX + (input.right * mImageWidth - x) * s, + offsetY + (input.bottom * mImageHeight - y) * s); + return output; + } + + @Override + protected void onCalculate(float progress) { + mCurrentX = Math.round(mStartX + (mTargetX - mStartX) * progress); + mCurrentY = Math.round(mStartY + (mTargetY - mStartY) * progress); + mCurrentScale = mStartScale + (mTargetScale - mStartScale) * progress; + + if (mCurrentX == mTargetX && mCurrentY == mTargetY + && mCurrentScale == mTargetScale) forceStop(); + } + + public int getCenterX() { + return mCurrentX; + } + + public int getCenterY() { + return mCurrentY; + } + + public float getScale() { + return mCurrentScale; + } + + private void calculateTarget(RectF highlight) { + float width = getWidth(); + float height = getHeight(); + + if (mImageWidth != SIZE_UNKNOWN) { + float minScale = Math.min(width / mImageWidth, height / mImageHeight); + float scale = Utils.clamp(SELECTION_RATIO * Math.min( + width / (highlight.width() * mImageWidth), + height / (highlight.height() * mImageHeight)), minScale, 2f); + int centerX = Math.round( + mImageWidth * (highlight.left + highlight.right) * 0.5f); + int centerY = Math.round( + mImageHeight * (highlight.top + highlight.bottom) * 0.5f); + + if (Math.round(mImageWidth * scale) > width) { + int limitX = Math.round(width * 0.5f / scale); + centerX = Math.round( + (highlight.left + highlight.right) * mImageWidth / 2); + centerX = Utils.clamp(centerX, limitX, mImageWidth - limitX); + } else { + centerX = mImageWidth / 2; + } + if (Math.round(mImageHeight * scale) > height) { + int limitY = Math.round(height * 0.5f / scale); + centerY = Math.round( + (highlight.top + highlight.bottom) * mImageHeight / 2); + centerY = Utils.clamp(centerY, limitY, mImageHeight - limitY); + } else { + centerY = mImageHeight / 2; + } + mTargetX = centerX; + mTargetY = centerY; + mTargetScale = scale; + } + } + + } + + private class HighlightRectangle extends GLView { + private RectF mHighlightRect = new RectF(0.25f, 0.25f, 0.75f, 0.75f); + private RectF mTempRect = new RectF(); + private PointF mTempPoint = new PointF(); + + private ResourceTexture mArrowX; + private ResourceTexture mArrowY; + + private int mMovingEdges = 0; + private float mReferenceX; + private float mReferenceY; + + public HighlightRectangle() { + mArrowX = new ResourceTexture(mActivity.getAndroidContext(), + R.drawable.camera_crop_width_holo); + mArrowY = new ResourceTexture(mActivity.getAndroidContext(), + R.drawable.camera_crop_height_holo); + } + + public void setInitRectangle() { + float targetRatio = mAspectRatio == UNSPECIFIED + ? 1f + : mAspectRatio * mImageHeight / mImageWidth; + float w = SELECTION_RATIO / 2f; + float h = SELECTION_RATIO / 2f; + if (targetRatio > 1) { + h = w / targetRatio; + } else { + w = h * targetRatio; + } + mHighlightRect.set(0.5f - w, 0.5f - h, 0.5f + w, 0.5f + h); + } + + public void setRectangle(RectF faceRect) { + mHighlightRect.set(faceRect); + mAnimation.startParkingAnimation(faceRect); + invalidate(); + } + + private void moveEdges(MotionEvent event) { + float scale = mAnimation.getScale(); + float dx = (event.getX() - mReferenceX) / scale / mImageWidth; + float dy = (event.getY() - mReferenceY) / scale / mImageHeight; + mReferenceX = event.getX(); + mReferenceY = event.getY(); + RectF r = mHighlightRect; + + if ((mMovingEdges & MOVE_BLOCK) != 0) { + dx = Utils.clamp(dx, -r.left, 1 - r.right); + dy = Utils.clamp(dy, -r.top , 1 - r.bottom); + r.top += dy; + r.bottom += dy; + r.left += dx; + r.right += dx; + } else { + PointF point = mTempPoint; + point.set(mReferenceX, mReferenceY); + mAnimation.inverseMapPoint(point); + float left = r.left + MIN_SELECTION_LENGTH / mImageWidth; + float right = r.right - MIN_SELECTION_LENGTH / mImageWidth; + float top = r.top + MIN_SELECTION_LENGTH / mImageHeight; + float bottom = r.bottom - MIN_SELECTION_LENGTH / mImageHeight; + if ((mMovingEdges & MOVE_RIGHT) != 0) { + r.right = Utils.clamp(point.x, left, 1f); + } + if ((mMovingEdges & MOVE_LEFT) != 0) { + r.left = Utils.clamp(point.x, 0, right); + } + if ((mMovingEdges & MOVE_TOP) != 0) { + r.top = Utils.clamp(point.y, 0, bottom); + } + if ((mMovingEdges & MOVE_BOTTOM) != 0) { + r.bottom = Utils.clamp(point.y, top, 1f); + } + if (mAspectRatio != UNSPECIFIED) { + float targetRatio = mAspectRatio * mImageHeight / mImageWidth; + if (r.width() / r.height() > targetRatio) { + float height = r.width() / targetRatio; + if ((mMovingEdges & MOVE_BOTTOM) != 0) { + r.bottom = Utils.clamp(r.top + height, top, 1f); + } else { + r.top = Utils.clamp(r.bottom - height, 0, bottom); + } + } else { + float width = r.height() * targetRatio; + if ((mMovingEdges & MOVE_LEFT) != 0) { + r.left = Utils.clamp(r.right - width, 0, right); + } else { + r.right = Utils.clamp(r.left + width, left, 1f); + } + } + if (r.width() / r.height() > targetRatio) { + float width = r.height() * targetRatio; + if ((mMovingEdges & MOVE_LEFT) != 0) { + r.left = Utils.clamp(r.right - width, 0, right); + } else { + r.right = Utils.clamp(r.left + width, left, 1f); + } + } else { + float height = r.width() / targetRatio; + if ((mMovingEdges & MOVE_BOTTOM) != 0) { + r.bottom = Utils.clamp(r.top + height, top, 1f); + } else { + r.top = Utils.clamp(r.bottom - height, 0, bottom); + } + } + } + } + invalidate(); + } + + private void setMovingEdges(MotionEvent event) { + RectF r = mAnimation.mapRect(mHighlightRect, mTempRect); + float x = event.getX(); + float y = event.getY(); + + if (x > r.left + TOUCH_TOLERANCE && x < r.right - TOUCH_TOLERANCE + && y > r.top + TOUCH_TOLERANCE && y < r.bottom - TOUCH_TOLERANCE) { + mMovingEdges = MOVE_BLOCK; + return; + } + + boolean inVerticalRange = (r.top - TOUCH_TOLERANCE) <= y + && y <= (r.bottom + TOUCH_TOLERANCE); + boolean inHorizontalRange = (r.left - TOUCH_TOLERANCE) <= x + && x <= (r.right + TOUCH_TOLERANCE); + + if (inVerticalRange) { + boolean left = Math.abs(x - r.left) <= TOUCH_TOLERANCE; + boolean right = Math.abs(x - r.right) <= TOUCH_TOLERANCE; + if (left && right) { + left = Math.abs(x - r.left) < Math.abs(x - r.right); + right = !left; + } + if (left) mMovingEdges |= MOVE_LEFT; + if (right) mMovingEdges |= MOVE_RIGHT; + if (mAspectRatio != UNSPECIFIED && inHorizontalRange) { + mMovingEdges |= (y > + (r.top + r.bottom) / 2) ? MOVE_BOTTOM : MOVE_TOP; + } + } + if (inHorizontalRange) { + boolean top = Math.abs(y - r.top) <= TOUCH_TOLERANCE; + boolean bottom = Math.abs(y - r.bottom) <= TOUCH_TOLERANCE; + if (top && bottom) { + top = Math.abs(y - r.top) < Math.abs(y - r.bottom); + bottom = !top; + } + if (top) mMovingEdges |= MOVE_TOP; + if (bottom) mMovingEdges |= MOVE_BOTTOM; + if (mAspectRatio != UNSPECIFIED && inVerticalRange) { + mMovingEdges |= (x > + (r.left + r.right) / 2) ? MOVE_RIGHT : MOVE_LEFT; + } + } + } + + @Override + protected boolean onTouch(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + mReferenceX = event.getX(); + mReferenceY = event.getY(); + setMovingEdges(event); + invalidate(); + return true; + } + case MotionEvent.ACTION_MOVE: + moveEdges(event); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + mMovingEdges = 0; + mAnimation.startParkingAnimation(mHighlightRect); + invalidate(); + return true; + } + } + return true; + } + + @Override + protected void renderBackground(GLCanvas canvas) { + RectF r = mAnimation.mapRect(mHighlightRect, mTempRect); + drawHighlightRectangle(canvas, r); + + float centerY = (r.top + r.bottom) / 2; + float centerX = (r.left + r.right) / 2; + if ((mMovingEdges & (MOVE_RIGHT | MOVE_BLOCK)) != 0) { + mArrowX.draw(canvas, + Math.round(r.right - mArrowX.getWidth() / 2), + Math.round(centerY - mArrowX.getHeight() / 2)); + } + if ((mMovingEdges & (MOVE_LEFT | MOVE_BLOCK)) != 0) { + mArrowX.draw(canvas, + Math.round(r.left - mArrowX.getWidth() / 2), + Math.round(centerY - mArrowX.getHeight() / 2)); + } + if ((mMovingEdges & (MOVE_TOP | MOVE_BLOCK)) != 0) { + mArrowY.draw(canvas, + Math.round(centerX - mArrowY.getWidth() / 2), + Math.round(r.top - mArrowY.getHeight() / 2)); + } + if ((mMovingEdges & (MOVE_BOTTOM | MOVE_BLOCK)) != 0) { + mArrowY.draw(canvas, + Math.round(centerX - mArrowY.getWidth() / 2), + Math.round(r.bottom - mArrowY.getHeight() / 2)); + } + } + + private void drawHighlightRectangle(GLCanvas canvas, RectF r) { + GL11 gl = canvas.getGLInstance(); + gl.glLineWidth(3.0f); + gl.glEnable(GL11.GL_LINE_SMOOTH); + + gl.glEnable(GL11.GL_STENCIL_TEST); + gl.glClear(GL11.GL_STENCIL_BUFFER_BIT); + gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_REPLACE); + gl.glStencilFunc(GL11.GL_ALWAYS, 1, 1); + + if (mSpotlightRatioX == 0 || mSpotlightRatioY == 0) { + canvas.fillRect(r.left, r.top, r.width(), r.height(), Color.TRANSPARENT); + canvas.drawRect(r.left, r.top, r.width(), r.height(), mPaint); + } else { + float sx = r.width() * mSpotlightRatioX; + float sy = r.height() * mSpotlightRatioY; + float cx = r.centerX(); + float cy = r.centerY(); + + canvas.fillRect(cx - sx / 2, cy - sy / 2, sx, sy, Color.TRANSPARENT); + canvas.drawRect(cx - sx / 2, cy - sy / 2, sx, sy, mPaint); + canvas.drawRect(r.left, r.top, r.width(), r.height(), mPaint); + + gl.glStencilFunc(GL11.GL_NOTEQUAL, 1, 1); + gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_REPLACE); + + canvas.drawRect(cx - sy / 2, cy - sx / 2, sy, sx, mPaint); + canvas.fillRect(cx - sy / 2, cy - sx / 2, sy, sx, Color.TRANSPARENT); + canvas.fillRect(r.left, r.top, r.width(), r.height(), 0x80000000); + } + + gl.glStencilFunc(GL11.GL_NOTEQUAL, 1, 1); + gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP); + + canvas.fillRect(0, 0, getWidth(), getHeight(), 0xA0000000); + + gl.glDisable(GL11.GL_STENCIL_TEST); + } + } + + private class DetectFaceTask extends Thread { + private final FaceDetector.Face[] mFaces = new FaceDetector.Face[MAX_FACE_COUNT]; + private final Bitmap mFaceBitmap; + private int mFaceCount; + + public DetectFaceTask(Bitmap bitmap) { + mFaceBitmap = bitmap; + setName("face-detect"); + } + + @Override + public void run() { + Bitmap bitmap = mFaceBitmap; + FaceDetector detector = new FaceDetector( + bitmap.getWidth(), bitmap.getHeight(), MAX_FACE_COUNT); + mFaceCount = detector.findFaces(bitmap, mFaces); + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_UPDATE_FACES, this)); + } + + private RectF getFaceRect(FaceDetector.Face face) { + PointF point = new PointF(); + face.getMidPoint(point); + + int width = mFaceBitmap.getWidth(); + int height = mFaceBitmap.getHeight(); + float rx = face.eyesDistance() * FACE_EYE_RATIO; + float ry = rx; + float aspect = mAspectRatio; + if (aspect != UNSPECIFIED) { + if (aspect > 1) { + rx = ry * aspect; + } else { + ry = rx / aspect; + } + } + + RectF r = new RectF( + point.x - rx, point.y - ry, point.x + rx, point.y + ry); + r.intersect(0, 0, width, height); + + if (aspect != UNSPECIFIED) { + if (r.width() / r.height() > aspect) { + float w = r.height() * aspect; + r.left = (r.left + r.right - w) * 0.5f; + r.right = r.left + w; + } else { + float h = r.width() / aspect; + r.top = (r.top + r.bottom - h) * 0.5f; + r.bottom = r.top + h; + } + } + + r.left /= width; + r.right /= width; + r.top /= height; + r.bottom /= height; + return r; + } + + public void updateFaces() { + if (mFaceCount > 1) { + for (int i = 0, n = mFaceCount; i < n; ++i) { + mFaceDetectionView.addFace(getFaceRect(mFaces[i])); + } + mFaceDetectionView.setVisibility(GLView.VISIBLE); + Toast.makeText(mActivity.getAndroidContext(), + R.string.multiface_crop_help, Toast.LENGTH_SHORT).show(); + } else if (mFaceCount == 1) { + mFaceDetectionView.setVisibility(GLView.INVISIBLE); + mHighlightRectangle.setRectangle(getFaceRect(mFaces[0])); + mHighlightRectangle.setVisibility(GLView.VISIBLE); + } else /*mFaceCount == 0*/ { + mHighlightRectangle.setInitRectangle(); + mHighlightRectangle.setVisibility(GLView.VISIBLE); + } + } + } + + public void setDataModel(TileImageView.Model dataModel, int rotation) { + if (((rotation / 90) & 0x01) != 0) { + mImageWidth = dataModel.getImageHeight(); + mImageHeight = dataModel.getImageWidth(); + } else { + mImageWidth = dataModel.getImageWidth(); + mImageHeight = dataModel.getImageHeight(); + } + + mImageRotation = rotation; + + mImageView.setModel(dataModel); + mAnimation.initialize(); + } + + public void detectFaces(Bitmap bitmap) { + int rotation = mImageRotation; + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + float scale = (float) Math.sqrt( + (double) FACE_PIXEL_COUNT / (width * height)); + + // faceBitmap is a correctly rotated bitmap, as viewed by a user. + Bitmap faceBitmap; + if (((rotation / 90) & 1) == 0) { + int w = (Math.round(width * scale) & ~1); // must be even + int h = Math.round(height * scale); + faceBitmap = Bitmap.createBitmap(w, h, Config.RGB_565); + Canvas canvas = new Canvas(faceBitmap); + canvas.rotate(rotation, w / 2, h / 2); + canvas.scale((float) w / width, (float) h / height); + canvas.drawBitmap(bitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG)); + } else { + int w = (Math.round(height * scale) & ~1); // must be even + int h = Math.round(width * scale); + faceBitmap = Bitmap.createBitmap(w, h, Config.RGB_565); + Canvas canvas = new Canvas(faceBitmap); + canvas.translate(w / 2, h / 2); + canvas.rotate(rotation); + canvas.translate(-h / 2, -w / 2); + canvas.scale((float) w / height, (float) h / width); + canvas.drawBitmap(bitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG)); + } + new DetectFaceTask(faceBitmap).start(); + } + + public void initializeHighlightRectangle() { + mHighlightRectangle.setInitRectangle(); + mHighlightRectangle.setVisibility(GLView.VISIBLE); + } + + public void resume() { + mImageView.prepareTextures(); + } + + public void pause() { + mImageView.freeTextures(); + } +} + diff --git a/src/com/android/gallery3d/ui/CustomMenu.java b/src/com/android/gallery3d/ui/CustomMenu.java new file mode 100644 index 000000000..de2367e60 --- /dev/null +++ b/src/com/android/gallery3d/ui/CustomMenu.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; + +import android.app.ActionBar; +import android.content.Context; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; + +import java.util.ArrayList; + +public class CustomMenu implements OnMenuItemClickListener { + @SuppressWarnings("unused") + private static final String TAG = "FilterMenu"; + + public static class DropDownMenu { + private Button mButton; + private PopupMenu mPopupMenu; + private Menu mMenu; + + public DropDownMenu(Context context, Button button, int menuId, + OnMenuItemClickListener listener) { + mButton = button; + mButton.setBackgroundDrawable(context.getResources().getDrawable( + R.drawable.dropdown_normal_holo_dark)); + mPopupMenu = new PopupMenu(context, mButton); + mMenu = mPopupMenu.getMenu(); + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + mPopupMenu.setOnMenuItemClickListener(listener); + mButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mPopupMenu.show(); + } + }); + } + + public MenuItem findItem(int id) { + return mMenu.findItem(id); + } + + public void setTitle(CharSequence title) { + mButton.setText(title); + } + } + + + + private Context mContext; + private ArrayList mMenus; + private OnMenuItemClickListener mListener; + + public CustomMenu(Context context) { + mContext = context; + mMenus = new ArrayList(); + } + + public DropDownMenu addDropDownMenu(Button button, int menuId) { + DropDownMenu menu = new DropDownMenu(mContext, button, menuId, this); + mMenus.add(menu); + return menu; + } + + public void setOnMenuItemClickListener(OnMenuItemClickListener listener) { + mListener = listener; + } + + public MenuItem findMenuItem(int id) { + MenuItem item = null; + for (DropDownMenu menu : mMenus) { + item = menu.findItem(id); + if (item != null) return item; + } + return item; + } + + public void setMenuItemAppliedEnabled(int id, boolean applied, boolean enabled, + boolean updateTitle) { + MenuItem item = null; + for (DropDownMenu menu : mMenus) { + item = menu.findItem(id); + if (item != null) { + item.setCheckable(true); + item.setChecked(applied); + item.setEnabled(enabled); + if (updateTitle) { + menu.setTitle(item.getTitle()); + } + } + } + } + + public void setMenuItemVisibility(int id, boolean visibility) { + MenuItem item = findMenuItem(id); + if (item != null) { + item.setVisible(visibility); + } + } + + public boolean onMenuItemClick(MenuItem item) { + if (mListener != null) { + return mListener.onMenuItemClick(item); + } + return false; + } +} diff --git a/src/com/android/gallery3d/ui/DetailsWindow.java b/src/com/android/gallery3d/ui/DetailsWindow.java new file mode 100644 index 000000000..03e216922 --- /dev/null +++ b/src/com/android/gallery3d/ui/DetailsWindow.java @@ -0,0 +1,451 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import static com.android.gallery3d.ui.DetailsWindowConfig.FONT_SIZE; +import static com.android.gallery3d.ui.DetailsWindowConfig.LEFT_RIGHT_EXTRA_PADDING; +import static com.android.gallery3d.ui.DetailsWindowConfig.LINE_SPACING; +import static com.android.gallery3d.ui.DetailsWindowConfig.PREFERRED_WIDTH; +import static com.android.gallery3d.ui.DetailsWindowConfig.TOP_BOTTOM_EXTRA_PADDING; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ReverseGeocoder; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; +import android.location.Address; +import android.os.Handler; +import android.os.Message; +import android.text.format.Formatter; +import android.view.MotionEvent; +import android.view.View.MeasureSpec; + +import java.util.ArrayList; +import java.util.Map.Entry; + +// TODO: Add scroll bar to this window. +public class DetailsWindow extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "DetailsWindow"; + private static final int MSG_REFRESH_LOCATION = 1; + private static final int FONT_COLOR = Color.WHITE; + private static final int CLOSE_BUTTON_SIZE = 32; + + private GalleryActivity mContext; + protected Texture mBackground; + private StringTexture mTitle; + private MyDataModel mModel; + private MediaDetails mDetails; + private DetailsSource mSource; + private int mIndex; + private int mLocationIndex; + private Future
mAddressLookupJob; + private Handler mHandler; + private Icon mCloseButton; + private int mMaxDetailLength; + private CloseListener mListener; + + private ScrollView mScrollView; + private DetailsPanel mDetailPanel = new DetailsPanel(); + + public interface DetailsSource { + public int size(); + public int findIndex(int indexHint); + public MediaDetails getDetails(); + } + + public interface CloseListener { + public void onClose(); + } + + public DetailsWindow(GalleryActivity activity, DetailsSource source) { + mContext = activity; + mSource = source; + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case MSG_REFRESH_LOCATION: + mModel.updateLocation((Address) msg.obj); + invalidate(); + break; + } + } + }; + Context context = activity.getAndroidContext(); + ResourceTexture icon = new ResourceTexture(context, R.drawable.ic_menu_cancel_holo_light); + setBackground(new NinePatchTexture(context, R.drawable.popup_full_dark)); + + mCloseButton = new Icon(context, icon, CLOSE_BUTTON_SIZE, CLOSE_BUTTON_SIZE) { + @Override + protected boolean onTouch(MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_UP: + if (mListener != null) mListener.onClose(); + } + return true; + } + }; + mScrollView = new ScrollView(context); + mScrollView.addComponent(mDetailPanel); + + super.addComponent(mScrollView); + super.addComponent(mCloseButton); + + reloadDetails(0); + } + + public void setCloseListener(CloseListener listener) { + mListener = listener; + } + + public void setBackground(Texture background) { + if (background == mBackground) return; + mBackground = background; + if (background != null && background instanceof NinePatchTexture) { + Rect p = ((NinePatchTexture) mBackground).getPaddings(); + p.left += LEFT_RIGHT_EXTRA_PADDING; + p.right += LEFT_RIGHT_EXTRA_PADDING; + p.top += TOP_BOTTOM_EXTRA_PADDING; + p.bottom += TOP_BOTTOM_EXTRA_PADDING; + setPaddings(p); + } else { + setPaddings(0, 0, 0, 0); + } + Rect p = getPaddings(); + mMaxDetailLength = PREFERRED_WIDTH - p.left - p.right; + invalidate(); + } + + public void setTitle(String title) { + mTitle = StringTexture.newInstance(title, FONT_SIZE, FONT_COLOR); + } + + @Override + protected void renderBackground(GLCanvas canvas) { + if (mBackground == null) return; + int width = getWidth(); + int height = getHeight(); + + //TODO: change alpha in the background image. + canvas.save(GLCanvas.SAVE_FLAG_ALPHA); + canvas.setAlpha(0.7f); + mBackground.draw(canvas, 0, 0, width, height); + canvas.restore(); + + Rect p = getPaddings(); + if (mTitle != null) mTitle.draw(canvas, p.left, p.top); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + int height = MeasureSpec.getSize(heightSpec); + MeasureHelper.getInstance(this) + .setPreferredContentSize(PREFERRED_WIDTH, height) + .measure(widthSpec, heightSpec); + } + + @Override + protected void onLayout(boolean sizeChange, int l, int t, int r, int b) { + mCloseButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int bWidth = mCloseButton.getMeasuredWidth(); + int bHeight = mCloseButton.getMeasuredHeight(); + int width = getWidth(); + int height = getHeight(); + + Rect p = getPaddings(); + mCloseButton.layout(width - p.right - bWidth, p.top, + width - p.right, p.top + bHeight); + mScrollView.layout(p.left, p.top + bHeight, width - p.right, + height - p.bottom); + } + + public void show() { + setVisibility(GLView.VISIBLE); + requestLayout(); + } + + public void hide() { + setVisibility(GLView.INVISIBLE); + requestLayout(); + } + + public void pause() { + Future
lookupJob = mAddressLookupJob; + if (lookupJob != null) { + lookupJob.cancel(); + lookupJob.waitDone(); + } + } + + public void reloadDetails(int indexHint) { + int index = mSource.findIndex(indexHint); + if (index == -1) return; + MediaDetails details = mSource.getDetails(); + if (details != null) { + if (mIndex == index && mDetails == details) return; + mIndex = index; + mDetails = details; + setDetails(details); + } + mDetailPanel.requestLayout(); + } + + private void setDetails(MediaDetails details) { + mModel = new MyDataModel(details); + invalidate(); + } + + private class AddressLookupJob implements Job
{ + double[] mLatlng; + protected AddressLookupJob(double[] latlng) { + mLatlng = latlng; + } + + public Address run(JobContext jc) { + ReverseGeocoder geocoder = new ReverseGeocoder(mContext.getAndroidContext()); + return geocoder.lookupAddress(mLatlng[0], mLatlng[1], true); + } + } + + private class MyDataModel { + ArrayList mItems; + + public MyDataModel(MediaDetails details) { + Context context = mContext.getAndroidContext(); + mLocationIndex = -1; + mItems = new ArrayList(details.size()); + setTitle(String.format(context.getString(R.string.sequence_in_set), + mIndex + 1, mSource.size())); + setDetails(context, details); + } + + private void setDetails(Context context, MediaDetails details) { + for (Entry detail : details) { + String value; + switch (detail.getKey()) { + case MediaDetails.INDEX_LOCATION: { + value = getLocationText((double[]) detail.getValue()); + break; + } + case MediaDetails.INDEX_SIZE: { + value = Formatter.formatFileSize( + context, (Long) detail.getValue()); + break; + } + case MediaDetails.INDEX_WHITE_BALANCE: { + value = "1".equals(detail.getValue()) + ? context.getString(R.string.manual) + : context.getString(R.string.auto); + break; + } + case MediaDetails.INDEX_FLASH: { + MediaDetails.FlashState flash = + (MediaDetails.FlashState) detail.getValue(); + // TODO: camera doesn't fill in the complete values, show more information + // when it is fixed. + if (flash.isFlashFired()) { + value = context.getString(R.string.flash_on); + } else { + value = context.getString(R.string.flash_off); + } + break; + } + case MediaDetails.INDEX_EXPOSURE_TIME: { + value = (String) detail.getValue(); + double time = Double.valueOf(value); + if (time < 1.0f) { + value = String.format("1/%d", (int) (0.5f + 1 / time)); + } else { + int integer = (int) time; + time -= integer; + value = String.valueOf(integer) + "''"; + if (time > 0.0001) { + value += String.format(" 1/%d", (int) (0.5f + 1 / time)); + } + } + break; + } + default: { + Object valueObj = detail.getValue(); + // This shouldn't happen, log its key to help us diagnose the problem. + Utils.assertTrue(valueObj != null, "%s's value is Null", + getName(context, detail.getKey())); + value = valueObj.toString(); + } + } + int key = detail.getKey(); + if (details.hasUnit(key)) { + value = String.format("%s : %s %s", getName(context, key), value, + context.getString(details.getUnit(key))); + } else { + value = String.format("%s : %s", getName(context, key), value); + } + Texture label = MultiLineTexture.newInstance( + value, mMaxDetailLength, FONT_SIZE, FONT_COLOR); + mItems.add(label); + } + } + + private String getLocationText(double[] latlng) { + String text = String.format("(%f, %f)", latlng[0], latlng[1]); + mAddressLookupJob = mContext.getThreadPool().submit( + new AddressLookupJob(latlng), + new FutureListener
() { + public void onFutureDone(Future
future) { + mAddressLookupJob = null; + if (!future.isCancelled()) { + mHandler.sendMessage(mHandler.obtainMessage( + MSG_REFRESH_LOCATION, future.get())); + } + } + }); + mLocationIndex = mItems.size(); + return text; + } + + public void updateLocation(Address address) { + int index = mLocationIndex; + if (address != null && index >=0 && index < mItems.size()) { + Context context = mContext.getAndroidContext(); + String parts[] = { + address.getAdminArea(), + address.getSubAdminArea(), + address.getLocality(), + address.getSubLocality(), + address.getThoroughfare(), + address.getSubThoroughfare(), + address.getPremises(), + address.getPostalCode(), + address.getCountryName() + }; + + String addressText = ""; + for (int i = 0; i < parts.length; i++) { + if (parts[i] == null || parts[i].isEmpty()) continue; + if (!addressText.isEmpty()) { + addressText += ", "; + } + addressText += parts[i]; + } + String text = String.format("%s : %s", getName(context, + MediaDetails.INDEX_LOCATION), addressText); + mItems.set(index, MultiLineTexture.newInstance( + text, mMaxDetailLength, FONT_SIZE, FONT_COLOR)); + } + } + + public Texture getView(int index) { + return mItems.get(index); + } + + public int size() { + return mItems.size(); + } + } + + private static String getName(Context context, int key) { + switch (key) { + case MediaDetails.INDEX_TITLE: + return context.getString(R.string.title); + case MediaDetails.INDEX_DESCRIPTION: + return context.getString(R.string.description); + case MediaDetails.INDEX_DATETIME: + return context.getString(R.string.time); + case MediaDetails.INDEX_LOCATION: + return context.getString(R.string.location); + case MediaDetails.INDEX_PATH: + return context.getString(R.string.path); + case MediaDetails.INDEX_WIDTH: + return context.getString(R.string.width); + case MediaDetails.INDEX_HEIGHT: + return context.getString(R.string.height); + case MediaDetails.INDEX_ORIENTATION: + return context.getString(R.string.orientation); + case MediaDetails.INDEX_DURATION: + return context.getString(R.string.duration); + case MediaDetails.INDEX_MIMETYPE: + return context.getString(R.string.mimetype); + case MediaDetails.INDEX_SIZE: + return context.getString(R.string.file_size); + case MediaDetails.INDEX_MAKE: + return context.getString(R.string.maker); + case MediaDetails.INDEX_MODEL: + return context.getString(R.string.model); + case MediaDetails.INDEX_FLASH: + return context.getString(R.string.flash); + case MediaDetails.INDEX_APERTURE: + return context.getString(R.string.aperture); + case MediaDetails.INDEX_FOCAL_LENGTH: + return context.getString(R.string.focal_length); + case MediaDetails.INDEX_WHITE_BALANCE: + return context.getString(R.string.white_balance); + case MediaDetails.INDEX_EXPOSURE_TIME: + return context.getString(R.string.exposure_time); + case MediaDetails.INDEX_ISO: + return context.getString(R.string.iso); + default: + return "Unknown key" + key; + } + } + + private class DetailsPanel extends GLView { + + @Override + public void onMeasure(int widthSpec, int heightSpec) { + if (mTitle == null || mModel == null) { + MeasureHelper.getInstance(this) + .setPreferredContentSize(PREFERRED_WIDTH, 0) + .measure(widthSpec, heightSpec); + return; + } + + int h = getPaddings().top + LINE_SPACING; + for (int i = 0, n = mModel.size(); i < n; ++i) { + h += mModel.getView(i).getHeight() + LINE_SPACING; + } + + MeasureHelper.getInstance(this) + .setPreferredContentSize(PREFERRED_WIDTH, h) + .measure(widthSpec, heightSpec); + } + + @Override + protected void render(GLCanvas canvas) { + super.render(canvas); + + if (mTitle == null || mModel == null) { + return; + } + Rect p = getPaddings(); + int x = p.left, y = p.top + LINE_SPACING; + for (int i = 0, n = mModel.size(); i < n ; i++) { + Texture t = mModel.getView(i); + t.draw(canvas, x, y); + y += t.getHeight() + LINE_SPACING; + } + } + } +} diff --git a/src/com/android/gallery3d/ui/DisplayItem.java b/src/com/android/gallery3d/ui/DisplayItem.java new file mode 100644 index 000000000..3038232f6 --- /dev/null +++ b/src/com/android/gallery3d/ui/DisplayItem.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +public abstract class DisplayItem { + + protected int mWidth; + protected int mHeight; + + protected void setSize(int width, int height) { + mWidth = width; + mHeight = height; + } + + // returns true if more pass is needed + public abstract boolean render(GLCanvas canvas, int pass); + + public abstract long getIdentity(); + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + public int getRotation() { + return 0; + } +} diff --git a/src/com/android/gallery3d/ui/DownUpDetector.java b/src/com/android/gallery3d/ui/DownUpDetector.java new file mode 100644 index 000000000..19db77262 --- /dev/null +++ b/src/com/android/gallery3d/ui/DownUpDetector.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.view.MotionEvent; + +public class DownUpDetector { + public interface DownUpListener { + void onDown(MotionEvent e); + void onUp(MotionEvent e); + } + + private boolean mStillDown; + private DownUpListener mListener; + + public DownUpDetector(DownUpListener listener) { + mListener = listener; + } + + private void setState(boolean down, MotionEvent e) { + if (down == mStillDown) return; + mStillDown = down; + if (down) { + mListener.onDown(e); + } else { + mListener.onUp(e); + } + } + + public void onTouchEvent(MotionEvent ev) { + switch (ev.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + setState(true, ev); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_POINTER_DOWN: // Multitouch event - abort. + setState(false, ev); + break; + } + } + + public boolean isDown() { + return mStillDown; + } +} diff --git a/src/com/android/gallery3d/ui/DrawableTexture.java b/src/com/android/gallery3d/ui/DrawableTexture.java new file mode 100644 index 000000000..5c3964d5c --- /dev/null +++ b/src/com/android/gallery3d/ui/DrawableTexture.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; + +// DrawableTexture is a texture whose content is from a Drawable. +public class DrawableTexture extends CanvasTexture { + + private final Drawable mDrawable; + + public DrawableTexture(Drawable drawable, int width, int height) { + super(width, height); + mDrawable = drawable; + } + + @Override + protected void onDraw(Canvas canvas, Bitmap backing) { + mDrawable.setBounds(0, 0, mWidth, mHeight); + mDrawable.draw(canvas); + } +} diff --git a/src/com/android/gallery3d/ui/FilmStripView.java b/src/com/android/gallery3d/ui/FilmStripView.java new file mode 100644 index 000000000..8d28f2c7b --- /dev/null +++ b/src/com/android/gallery3d/ui/FilmStripView.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.anim.AlphaAnimation; +import com.android.gallery3d.anim.CanvasAnimation; +import com.android.gallery3d.app.AlbumDataAdapter; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.data.MediaSet; + +import android.content.Context; +import android.view.MotionEvent; +import android.view.View.MeasureSpec; + +public class FilmStripView extends GLView implements SlotView.Listener, + ScrollBarView.Listener, UserInteractionListener { + @SuppressWarnings("unused") + private static final String TAG = "FilmStripView"; + + private static final int HIDE_ANIMATION_DURATION = 300; // 0.3 sec + + public interface Listener { + void onSlotSelected(int slotIndex); + } + + private int mTopMargin, mMidMargin, mBottomMargin; + private int mContentSize, mBarSize, mGripSize; + private AlbumView mAlbumView; + private ScrollBarView mScrollBarView; + private AlbumDataAdapter mAlbumDataAdapter; + private StripDrawer mStripDrawer; + private Listener mListener; + private UserInteractionListener mUIListener; + private boolean mFilmStripVisible; + private CanvasAnimation mFilmStripAnimation; + private NinePatchTexture mBackgroundTexture; + + // The layout of FileStripView is + // topMargin + // ----+----+ + // / +----+--\ + // contentSize | | thumbSize + // \ +----+--/ + // ----+----+ + // midMargin + // ----+----+ + // / +----+--\ + // barSize | | gripSize + // \ +----+--/ + // ----+----+ + // bottomMargin + public FilmStripView(GalleryActivity activity, MediaSet mediaSet, + int topMargin, int midMargin, int bottomMargin, int contentSize, + int thumbSize, int barSize, int gripSize, int gripWidth) { + mTopMargin = topMargin; + mMidMargin = midMargin; + mBottomMargin = bottomMargin; + mContentSize = contentSize; + mBarSize = barSize; + mGripSize = gripSize; + + mStripDrawer = new StripDrawer((Context) activity); + mAlbumView = new AlbumView(activity, thumbSize, thumbSize, thumbSize); + mAlbumView.setOverscrollEffect(SlotView.OVERSCROLL_SYSTEM); + mAlbumView.setSelectionDrawer(mStripDrawer); + mAlbumView.setListener(this); + mAlbumView.setUserInteractionListener(this); + mAlbumDataAdapter = new AlbumDataAdapter(activity, mediaSet); + addComponent(mAlbumView); + mScrollBarView = new ScrollBarView(activity.getAndroidContext(), + mGripSize, gripWidth); + mScrollBarView.setListener(this); + addComponent(mScrollBarView); + + mAlbumView.setModel(mAlbumDataAdapter); + mBackgroundTexture = new NinePatchTexture(activity.getAndroidContext(), + R.drawable.navstrip_translucent); + mFilmStripVisible = true; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public void setUserInteractionListener(UserInteractionListener listener) { + mUIListener = listener; + } + + private void setFilmStripVisible(boolean visible) { + if (mFilmStripVisible == visible) return; + mFilmStripVisible = visible; + if (!visible) { + mFilmStripAnimation = new AlphaAnimation(1, 0); + mFilmStripAnimation.setDuration(HIDE_ANIMATION_DURATION); + mFilmStripAnimation.start(); + } else { + mFilmStripAnimation = null; + } + invalidate(); + } + + public void show() { + setFilmStripVisible(true); + } + + public void hide() { + setFilmStripVisible(false); + } + + @Override + protected void onVisibilityChanged(int visibility) { + super.onVisibilityChanged(visibility); + if (visibility == GLView.VISIBLE) { + onUserInteraction(); + } + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + int height = mTopMargin + mContentSize + mMidMargin + mBarSize + mBottomMargin; + MeasureHelper.getInstance(this) + .setPreferredContentSize(MeasureSpec.getSize(widthSpec), height) + .measure(widthSpec, heightSpec); + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + if (!changed) return; + mAlbumView.layout(0, mTopMargin, right - left, mTopMargin + mContentSize); + int barStart = mTopMargin + mContentSize + mMidMargin; + mScrollBarView.layout(0, barStart, right - left, barStart + mBarSize); + int width = right - left; + int height = bottom - top; + } + + @Override + protected boolean onTouch(MotionEvent event) { + // consume all touch events on the "gray area", so they don't go to + // the photo view below. (otherwise you can scroll the picture through + // it). + return true; + } + + @Override + protected boolean dispatchTouchEvent(MotionEvent event) { + if (!mFilmStripVisible && mFilmStripAnimation == null) { + return false; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + onUserInteractionBegin(); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + onUserInteractionEnd(); + break; + } + + return super.dispatchTouchEvent(event); + } + + @Override + protected void render(GLCanvas canvas) { + CanvasAnimation anim = mFilmStripAnimation; + if (anim == null && !mFilmStripVisible) return; + + boolean needRestore = false; + if (anim != null) { + needRestore = true; + canvas.save(anim.getCanvasSaveFlags()); + long now = canvas.currentAnimationTimeMillis(); + boolean more = anim.calculate(now); + anim.apply(canvas); + if (more) { + invalidate(); + } else { + mFilmStripAnimation = null; + } + } + + mBackgroundTexture.draw(canvas, 0, 0, getWidth(), getHeight()); + super.render(canvas); + + if (needRestore) { + canvas.restore(); + } + } + + // Called by AlbumView + public void onSingleTapUp(int slotIndex) { + mAlbumView.setFocusIndex(slotIndex); + mListener.onSlotSelected(slotIndex); + } + + // Called by AlbumView + public void onLongTap(int slotIndex) { + onSingleTapUp(slotIndex); + } + + // Called by AlbumView + public void onUserInteractionBegin() { + mUIListener.onUserInteractionBegin(); + } + + // Called by AlbumView + public void onUserInteractionEnd() { + mUIListener.onUserInteractionEnd(); + } + + // Called by AlbumView + public void onUserInteraction() { + mUIListener.onUserInteraction(); + } + + // Called by AlbumView + public void onScrollPositionChanged(int position, int total) { + mScrollBarView.setContentPosition(position, total); + } + + // Called by ScrollBarView + public void onScrollBarPositionChanged(int position) { + mAlbumView.setScrollPosition(position); + } + + public void setFocusIndex(int slotIndex) { + mAlbumView.setFocusIndex(slotIndex); + mAlbumView.makeSlotVisible(slotIndex); + } + + public void setStartIndex(int slotIndex) { + mAlbumView.setStartIndex(slotIndex); + } + + public void pause() { + mAlbumView.pause(); + mAlbumDataAdapter.pause(); + } + + public void resume() { + mAlbumView.resume(); + mAlbumDataAdapter.resume(); + } +} diff --git a/src/com/android/gallery3d/ui/GLCanvas.java b/src/com/android/gallery3d/ui/GLCanvas.java new file mode 100644 index 000000000..88c02f3b5 --- /dev/null +++ b/src/com/android/gallery3d/ui/GLCanvas.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.RectF; + +import javax.microedition.khronos.opengles.GL11; + +// +// GLCanvas gives a convenient interface to draw using OpenGL. +// +// When a rectangle is specified in this interface, it means the region +// [x, x+width) * [y, y+height) +// +public interface GLCanvas { + // Tells GLCanvas the size of the underlying GL surface. This should be + // called before first drawing and when the size of GL surface is changed. + // This is called by GLRoot and should not be called by the clients + // who only want to draw on the GLCanvas. Both width and height must be + // nonnegative. + public void setSize(int width, int height); + + // Clear the drawing buffers. This should only be used by GLRoot. + public void clearBuffer(); + + // This is the time value used to calculate the animation in the current + // frame. The "set" function should only called by GLRoot, and the + // "time" parameter must be nonnegative. + public void setCurrentAnimationTimeMillis(long time); + public long currentAnimationTimeMillis(); + + public void setBlendEnabled(boolean enabled); + + // Sets and gets the current alpha, alpha must be in [0, 1]. + public void setAlpha(float alpha); + public float getAlpha(); + + // (current alpha) = (current alpha) * alpha + public void multiplyAlpha(float alpha); + + // Change the current transform matrix. + public void translate(float x, float y, float z); + public void scale(float sx, float sy, float sz); + public void rotate(float angle, float x, float y, float z); + public void multiplyMatrix(float[] mMatrix, int offset); + + // Modifies the current clip with the specified rectangle. + // (current clip) = (current clip) intersect (specified rectangle). + // Returns true if the result clip is non-empty. + public boolean clipRect(int left, int top, int right, int bottom); + + // Pushes the configuration state (matrix, alpha, and clip) onto + // a private stack. + public int save(); + + // Same as save(), but only save those specified in saveFlags. + public int save(int saveFlags); + + public static final int SAVE_FLAG_ALL = 0xFFFFFFFF; + public static final int SAVE_FLAG_CLIP = 0x01; + public static final int SAVE_FLAG_ALPHA = 0x02; + public static final int SAVE_FLAG_MATRIX = 0x04; + + // Pops from the top of the stack as current configuration state (matrix, + // alpha, and clip). This call balances a previous call to save(), and is + // used to remove all modifications to the configuration state since the + // last save call. + public void restore(); + + // Draws a line using the specified paint from (x1, y1) to (x2, y2). + // (Both end points are included). + public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint); + + // Draws a rectangle using the specified paint from (x1, y1) to (x2, y2). + // (Both end points are included). + public void drawRect(float x1, float y1, float x2, float y2, GLPaint paint); + + // Fills the specified rectangle with the specified color. + public void fillRect(float x, float y, float width, float height, int color); + + // Draws a texture to the specified rectangle. + public void drawTexture( + BasicTexture texture, int x, int y, int width, int height); + public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer, + int uvBuffer, int indexBuffer, int indexCount); + + // Draws a texture to the specified rectangle. The "alpha" parameter + // overrides the current drawing alpha value. + public void drawTexture(BasicTexture texture, + int x, int y, int width, int height, float alpha); + + // Draws a the source rectangle part of the texture to the target rectangle. + public void drawTexture(BasicTexture texture, RectF source, RectF target); + + // Draw two textures to the specified rectangle. The actual texture used is + // from * (1 - ratio) + to * ratio + // The two textures must have the same size. + public void drawMixed(BasicTexture from, BasicTexture to, + float ratio, int x, int y, int w, int h); + + public void drawMixed(BasicTexture from, int toColor, + float ratio, int x, int y, int w, int h); + + // Return a texture copied from the specified rectangle. + public BasicTexture copyTexture(int x, int y, int width, int height); + + // Gets the underlying GL instance. This is used only when direct access to + // GL is needed. + public GL11 getGLInstance(); + + // Unloads the specified texture from the canvas. The resource allocated + // to draw the texture will be released. The specified texture will return + // to the unloaded state. This function should be called only from + // BasicTexture or its descendant + public boolean unloadTexture(BasicTexture texture); + + // Delete the specified buffer object, similar to unloadTexture. + public void deleteBuffer(int bufferId); + + // Delete the textures and buffers in GL side. This function should only be + // called in the GL thread. + public void deleteRecycledResources(); + +} diff --git a/src/com/android/gallery3d/ui/GLCanvasImpl.java b/src/com/android/gallery3d/ui/GLCanvasImpl.java new file mode 100644 index 000000000..387743f5d --- /dev/null +++ b/src/com/android/gallery3d/ui/GLCanvasImpl.java @@ -0,0 +1,913 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.IntArray; + +import android.graphics.Rect; +import android.graphics.RectF; +import android.opengl.GLU; +import android.opengl.Matrix; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.Stack; +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11Ext; + +public class GLCanvasImpl implements GLCanvas { + @SuppressWarnings("unused") + private static final String TAG = "GLCanvasImp"; + + private static final float OPAQUE_ALPHA = 0.95f; + + private static final int OFFSET_FILL_RECT = 0; + private static final int OFFSET_DRAW_LINE = 4; + private static final int OFFSET_DRAW_RECT = 6; + private static final float[] BOX_COORDINATES = { + 0, 0, 1, 0, 0, 1, 1, 1, // used for filling a rectangle + 0, 0, 1, 1, // used for drawing a line + 0, 0, 0, 1, 1, 1, 1, 0}; // used for drawing the outline of a rectangle + + private final GL11 mGL; + + private final float mMatrixValues[] = new float[16]; + private final float mTextureMatrixValues[] = new float[16]; + + // mapPoints needs 10 input and output numbers. + private final float mMapPointsBuffer[] = new float[10]; + + private final float mTextureColor[] = new float[4]; + + private int mBoxCoords; + + private final GLState mGLState; + + private long mAnimationTime; + + private float mAlpha; + private final Rect mClipRect = new Rect(); + private final Stack mRestoreStack = + new Stack(); + private ConfigState mRecycledRestoreAction; + + private final RectF mDrawTextureSourceRect = new RectF(); + private final RectF mDrawTextureTargetRect = new RectF(); + private final float[] mTempMatrix = new float[32]; + private final IntArray mUnboundTextures = new IntArray(); + private final IntArray mDeleteBuffers = new IntArray(); + private int mHeight; + private boolean mBlendEnabled = true; + + // Drawing statistics + int mCountDrawLine; + int mCountFillRect; + int mCountDrawMesh; + int mCountTextureRect; + int mCountTextureOES; + + GLCanvasImpl(GL11 gl) { + mGL = gl; + mGLState = new GLState(gl); + initialize(); + } + + public void setSize(int width, int height) { + Utils.assertTrue(width >= 0 && height >= 0); + mHeight = height; + + GL11 gl = mGL; + gl.glViewport(0, 0, width, height); + gl.glMatrixMode(GL11.GL_PROJECTION); + gl.glLoadIdentity(); + GLU.gluOrtho2D(gl, 0, width, 0, height); + + gl.glMatrixMode(GL11.GL_MODELVIEW); + gl.glLoadIdentity(); + float matrix[] = mMatrixValues; + + Matrix.setIdentityM(matrix, 0); + Matrix.translateM(matrix, 0, 0, mHeight, 0); + Matrix.scaleM(matrix, 0, 1, -1, 1); + + mClipRect.set(0, 0, width, height); + gl.glScissor(0, 0, width, height); + } + + public long currentAnimationTimeMillis() { + return mAnimationTime; + } + + public void setAlpha(float alpha) { + Utils.assertTrue(alpha >= 0 && alpha <= 1); + mAlpha = alpha; + } + + public void multiplyAlpha(float alpha) { + Utils.assertTrue(alpha >= 0 && alpha <= 1); + mAlpha *= alpha; + } + + public float getAlpha() { + return mAlpha; + } + + private static ByteBuffer allocateDirectNativeOrderBuffer(int size) { + return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); + } + + private void initialize() { + GL11 gl = mGL; + + // First create an nio buffer, then create a VBO from it. + int size = BOX_COORDINATES.length * Float.SIZE / Byte.SIZE; + FloatBuffer xyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); + xyBuffer.put(BOX_COORDINATES, 0, BOX_COORDINATES.length).position(0); + + int[] name = new int[1]; + gl.glGenBuffers(1, name, 0); + mBoxCoords = name[0]; + + gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords); + gl.glBufferData(GL11.GL_ARRAY_BUFFER, + xyBuffer.capacity() * (Float.SIZE / Byte.SIZE), + xyBuffer, GL11.GL_STATIC_DRAW); + + gl.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); + gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + + // Enable the texture coordinate array for Texture 1 + gl.glClientActiveTexture(GL11.GL_TEXTURE1); + gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + gl.glClientActiveTexture(GL11.GL_TEXTURE0); + gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); + + // mMatrixValues will be initialized in setSize() + mAlpha = 1.0f; + } + + public void drawRect(float x, float y, float width, float height, GLPaint paint) { + GL11 gl = mGL; + + mGLState.setColorMode(paint.getColor(), mAlpha); + mGLState.setLineWidth(paint.getLineWidth()); + mGLState.setLineSmooth(paint.getAntiAlias()); + + saveTransform(); + translate(x, y, 0); + scale(width, height, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_LINE_LOOP, OFFSET_DRAW_RECT, 4); + + restoreTransform(); + mCountDrawLine++; + } + + public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) { + GL11 gl = mGL; + + mGLState.setColorMode(paint.getColor(), mAlpha); + mGLState.setLineWidth(paint.getLineWidth()); + mGLState.setLineSmooth(paint.getAntiAlias()); + + saveTransform(); + translate(x1, y1, 0); + scale(x2 - x1, y2 - y1, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_LINE_STRIP, OFFSET_DRAW_LINE, 2); + + restoreTransform(); + mCountDrawLine++; + } + + public void fillRect(float x, float y, float width, float height, int color) { + mGLState.setColorMode(color, mAlpha); + GL11 gl = mGL; + + saveTransform(); + translate(x, y, 0); + scale(width, height, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4); + + restoreTransform(); + mCountFillRect++; + } + + public void translate(float x, float y, float z) { + Matrix.translateM(mMatrixValues, 0, x, y, z); + } + + public void scale(float sx, float sy, float sz) { + Matrix.scaleM(mMatrixValues, 0, sx, sy, sz); + } + + public void rotate(float angle, float x, float y, float z) { + float[] temp = mTempMatrix; + Matrix.setRotateM(temp, 0, angle, x, y, z); + Matrix.multiplyMM(temp, 16, mMatrixValues, 0, temp, 0); + System.arraycopy(temp, 16, mMatrixValues, 0, 16); + } + + public void multiplyMatrix(float matrix[], int offset) { + float[] temp = mTempMatrix; + Matrix.multiplyMM(temp, 0, mMatrixValues , 0, matrix, 0); + System.arraycopy(temp, 0, mMatrixValues, 0, 16); + } + + private void textureRect(float x, float y, float width, float height) { + GL11 gl = mGL; + + saveTransform(); + translate(x, y, 0); + scale(width, height, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4); + + restoreTransform(); + mCountTextureRect++; + } + + public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer, + int uvBuffer, int indexBuffer, int indexCount) { + float alpha = mAlpha; + if (!bindTexture(tex)) return; + + mGLState.setBlendEnabled(mBlendEnabled + && (!tex.isOpaque() || alpha < OPAQUE_ALPHA)); + mGLState.setTextureAlpha(alpha); + + // Reset the texture matrix. We will set our own texture coordinates + // below. + setTextureCoords(0, 0, 1, 1); + + saveTransform(); + translate(x, y, 0); + + mGL.glLoadMatrixf(mMatrixValues, 0); + + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, xyBuffer); + mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); + + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, uvBuffer); + mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + + mGL.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, indexBuffer); + mGL.glDrawElements(GL11.GL_TRIANGLE_STRIP, + indexCount, GL11.GL_UNSIGNED_BYTE, 0); + + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords); + mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); + mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + + restoreTransform(); + mCountDrawMesh++; + } + + private float[] mapPoints(float matrix[], int x1, int y1, int x2, int y2) { + float[] point = mMapPointsBuffer; + int srcOffset = 6; + point[srcOffset] = x1; + point[srcOffset + 1] = y1; + point[srcOffset + 2] = 0; + point[srcOffset + 3] = 1; + + int resultOffset = 0; + Matrix.multiplyMV(point, resultOffset, matrix, 0, point, srcOffset); + point[resultOffset] /= point[resultOffset + 3]; + point[resultOffset + 1] /= point[resultOffset + 3]; + + // map the second point + point[srcOffset] = x2; + point[srcOffset + 1] = y2; + resultOffset = 2; + Matrix.multiplyMV(point, resultOffset, matrix, 0, point, srcOffset); + point[resultOffset] /= point[resultOffset + 3]; + point[resultOffset + 1] /= point[resultOffset + 3]; + + return point; + } + + public boolean clipRect(int left, int top, int right, int bottom) { + float point[] = mapPoints(mMatrixValues, left, top, right, bottom); + + // mMatrix could be a rotation matrix. In this case, we need to find + // the boundaries after rotation. (only handle 90 * n degrees) + if (point[0] > point[2]) { + left = (int) point[2]; + right = (int) point[0]; + } else { + left = (int) point[0]; + right = (int) point[2]; + } + if (point[1] > point[3]) { + top = (int) point[3]; + bottom = (int) point[1]; + } else { + top = (int) point[1]; + bottom = (int) point[3]; + } + Rect clip = mClipRect; + + boolean intersect = clip.intersect(left, top, right, bottom); + if (!intersect) clip.set(0, 0, 0, 0); + mGL.glScissor(clip.left, clip.top, clip.width(), clip.height()); + return intersect; + } + + private void drawBoundTexture( + BasicTexture texture, int x, int y, int width, int height) { + // Test whether it has been rotated or flipped, if so, glDrawTexiOES + // won't work + if (isMatrixRotatedOrFlipped(mMatrixValues)) { + setTextureCoords(0, 0, + (float) texture.getWidth() / texture.getTextureWidth(), + (float) texture.getHeight() / texture.getTextureHeight()); + textureRect(x, y, width, height); + } else { + // draw the rect from bottom-left to top-right + float points[] = mapPoints( + mMatrixValues, x, y + height, x + width, y); + x = Math.round(points[0]); + y = Math.round(points[1]); + width = Math.round(points[2]) - x; + height = Math.round(points[3]) - y; + if (width > 0 && height > 0) { + ((GL11Ext) mGL).glDrawTexiOES(x, y, 0, width, height); + mCountTextureOES++; + } + } + } + + public void drawTexture( + BasicTexture texture, int x, int y, int width, int height) { + drawTexture(texture, x, y, width, height, mAlpha); + } + + public void setBlendEnabled(boolean enabled) { + mBlendEnabled = enabled; + } + + public void drawTexture(BasicTexture texture, + int x, int y, int width, int height, float alpha) { + if (width <= 0 || height <= 0) return; + + mGLState.setBlendEnabled(mBlendEnabled + && (!texture.isOpaque() || alpha < OPAQUE_ALPHA)); + if (!bindTexture(texture)) return; + mGLState.setTextureAlpha(alpha); + drawBoundTexture(texture, x, y, width, height); + } + + public void drawTexture(BasicTexture texture, RectF source, RectF target) { + if (target.width() <= 0 || target.height() <= 0) return; + + // Copy the input to avoid changing it. + mDrawTextureSourceRect.set(source); + mDrawTextureTargetRect.set(target); + source = mDrawTextureSourceRect; + target = mDrawTextureTargetRect; + + mGLState.setBlendEnabled(mBlendEnabled + && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA)); + if (!bindTexture(texture)) return; + convertCoordinate(source, target, texture); + setTextureCoords(source); + mGLState.setTextureAlpha(mAlpha); + textureRect(target.left, target.top, target.width(), target.height()); + } + + // This function changes the source coordinate to the texture coordinates. + // It also clips the source and target coordinates if it is beyond the + // bound of the texture. + private void convertCoordinate(RectF source, RectF target, + BasicTexture texture) { + + int width = texture.getWidth(); + int height = texture.getHeight(); + int texWidth = texture.getTextureWidth(); + int texHeight = texture.getTextureHeight(); + // Convert to texture coordinates + source.left /= texWidth; + source.right /= texWidth; + source.top /= texHeight; + source.bottom /= texHeight; + + // Clip if the rendering range is beyond the bound of the texture. + float xBound = (float) width / texWidth; + if (source.right > xBound) { + target.right = target.left + target.width() * + (xBound - source.left) / source.width(); + source.right = xBound; + } + float yBound = (float) height / texHeight; + if (source.bottom > yBound) { + target.bottom = target.top + target.height() * + (yBound - source.top) / source.height(); + source.bottom = yBound; + } + } + + public void drawMixed(BasicTexture from, + int toColor, float ratio, int x, int y, int w, int h) { + drawMixed(from, toColor, ratio, x, y, w, h, mAlpha); + } + + public void drawMixed(BasicTexture from, BasicTexture to, + float ratio, int x, int y, int w, int h) { + drawMixed(from, to, ratio, x, y, w, h, mAlpha); + } + + private boolean bindTexture(BasicTexture texture) { + if (!texture.onBind(this)) return false; + mGLState.setTexture2DEnabled(true); + mGL.glBindTexture(GL11.GL_TEXTURE_2D, texture.getId()); + return true; + } + + private void setTextureColor(float r, float g, float b, float alpha) { + float[] color = mTextureColor; + color[0] = r; + color[1] = g; + color[2] = b; + color[3] = alpha; + } + + private void drawMixed(BasicTexture from, int toColor, + float ratio, int x, int y, int width, int height, float alpha) { + + if (ratio <= 0) { + drawTexture(from, x, y, width, height, alpha); + return; + } else if (ratio >= 1) { + fillRect(x, y, width, height, toColor); + return; + } + + mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque() + || !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA)); + + final GL11 gl = mGL; + if (!bindTexture(from)) return; + + // + // The formula we want: + // alpha * ((1 - ratio) * from + ratio * to) + // The formula that GL supports is in the form of: + // combo * (modulate * from) + (1 - combo) * to + // + // So, we have combo = 1 - alpha * ratio + // and modulate = alpha * (1f - ratio) / combo + // + float comboRatio = 1 - alpha * ratio; + + // handle the case that (1 - comboRatio) == 0 + if (alpha < OPAQUE_ALPHA) { + mGLState.setTextureAlpha(alpha * (1f - ratio) / comboRatio); + } else { + mGLState.setTextureAlpha(1f); + } + + // Interpolate the RGB and alpha values between both textures. + mGLState.setTexEnvMode(GL11.GL_COMBINE); + // Specify the interpolation factor via the alpha component of + // GL_TEXTURE_ENV_COLORs. + // RGB component are get from toColor and will used as SRC1 + float colorAlpha = (float) (toColor >>> 24) / (0xff * 0xff); + setTextureColor(((toColor >>> 16) & 0xff) * colorAlpha, + ((toColor >>> 8) & 0xff) * colorAlpha, + (toColor & 0xff) * colorAlpha, comboRatio); + gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0); + + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_ALPHA, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA); + + // Wire up the interpolation factor for RGB. + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA); + + // Wire up the interpolation factor for alpha. + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA); + + drawBoundTexture(from, x, y, width, height); + mGLState.setTexEnvMode(GL11.GL_REPLACE); + } + + private void drawMixed(BasicTexture from, BasicTexture to, + float ratio, int x, int y, int width, int height, float alpha) { + + if (ratio <= 0) { + drawTexture(from, x, y, width, height, alpha); + return; + } else if (ratio >= 1) { + drawTexture(to, x, y, width, height, alpha); + return; + } + + // In the current implementation the two textures must have the + // same size. + Utils.assertTrue(from.getWidth() == to.getWidth() + && from.getHeight() == to.getHeight()); + + mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque() + || !to.isOpaque() || alpha < OPAQUE_ALPHA)); + + final GL11 gl = mGL; + if (!bindTexture(from)) return; + + // + // The formula we want: + // alpha * ((1 - ratio) * from + ratio * to) + // The formula that GL supports is in the form of: + // combo * (modulate * from) + (1 - combo) * to + // + // So, we have combo = 1 - alpha * ratio + // and modulate = alpha * (1f - ratio) / combo + // + float comboRatio = 1 - alpha * ratio; + + // handle the case that (1 - comboRatio) == 0 + if (alpha < OPAQUE_ALPHA) { + mGLState.setTextureAlpha(alpha * (1f - ratio) / comboRatio); + } else { + mGLState.setTextureAlpha(1f); + } + + gl.glActiveTexture(GL11.GL_TEXTURE1); + if (!bindTexture(to)) { + // Disable TEXTURE1. + gl.glDisable(GL11.GL_TEXTURE_2D); + // Switch back to the default texture unit. + gl.glActiveTexture(GL11.GL_TEXTURE0); + return; + } + gl.glEnable(GL11.GL_TEXTURE_2D); + + // Interpolate the RGB and alpha values between both textures. + mGLState.setTexEnvMode(GL11.GL_COMBINE); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE); + + // Specify the interpolation factor via the alpha component of + // GL_TEXTURE_ENV_COLORs. + // We don't use the RGB color, so just give them 0s. + setTextureColor(0, 0, 0, comboRatio); + gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0); + + // Wire up the interpolation factor for RGB. + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA); + + // Wire up the interpolation factor for alpha. + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA); + + // Draw the combined texture. + drawBoundTexture(to, x, y, width, height); + + // Disable TEXTURE1. + gl.glDisable(GL11.GL_TEXTURE_2D); + // Switch back to the default texture unit. + gl.glActiveTexture(GL11.GL_TEXTURE0); + } + + // TODO: the code only work for 2D should get fixed for 3D or removed + private static final int MSKEW_X = 4; + private static final int MSKEW_Y = 1; + private static final int MSCALE_X = 0; + private static final int MSCALE_Y = 5; + + private static boolean isMatrixRotatedOrFlipped(float matrix[]) { + final float eps = 1e-5f; + return Math.abs(matrix[MSKEW_X]) > eps + || Math.abs(matrix[MSKEW_Y]) > eps + || matrix[MSCALE_X] < -eps + || matrix[MSCALE_Y] > eps; + } + + public BasicTexture copyTexture(int x, int y, int width, int height) { + + if (isMatrixRotatedOrFlipped(mMatrixValues)) { + throw new IllegalArgumentException("cannot support rotated matrix"); + } + float points[] = mapPoints(mMatrixValues, x, y + height, x + width, y); + x = (int) points[0]; + y = (int) points[1]; + width = (int) points[2] - x; + height = (int) points[3] - y; + + GL11 gl = mGL; + + RawTexture texture = RawTexture.newInstance(this); + gl.glBindTexture(GL11.GL_TEXTURE_2D, texture.getId()); + texture.setSize(width, height); + + int[] cropRect = {0, 0, width, height}; + gl.glTexParameteriv(GL11.GL_TEXTURE_2D, + GL11Ext.GL_TEXTURE_CROP_RECT_OES, cropRect, 0); + gl.glTexParameteri(GL11.GL_TEXTURE_2D, + GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL11.GL_TEXTURE_2D, + GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE); + gl.glTexParameterf(GL11.GL_TEXTURE_2D, + GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + gl.glTexParameterf(GL11.GL_TEXTURE_2D, + GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + gl.glCopyTexImage2D(GL11.GL_TEXTURE_2D, 0, + GL11.GL_RGB, x, y, texture.getTextureWidth(), + texture.getTextureHeight(), 0); + + return texture; + } + + private static class GLState { + + private final GL11 mGL; + + private int mTexEnvMode = GL11.GL_REPLACE; + private float mTextureAlpha = 1.0f; + private boolean mTexture2DEnabled = true; + private boolean mBlendEnabled = true; + private float mLineWidth = 1.0f; + private boolean mLineSmooth = false; + + public GLState(GL11 gl) { + mGL = gl; + + // Disable unused state + gl.glDisable(GL11.GL_LIGHTING); + + // Enable used features + gl.glEnable(GL11.GL_DITHER); + gl.glEnable(GL11.GL_SCISSOR_TEST); + + gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); + gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); + gl.glEnable(GL11.GL_TEXTURE_2D); + + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, + GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE); + + // Set the background color + gl.glClearColor(0f, 0f, 0f, 0f); + gl.glClearStencil(0); + + gl.glEnable(GL11.GL_BLEND); + gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); + + // We use 565 or 8888 format, so set the alignment to 2 bytes/pixel. + gl.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 2); + } + + public void setTexEnvMode(int mode) { + if (mTexEnvMode == mode) return; + mTexEnvMode = mode; + mGL.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, mode); + } + + public void setLineWidth(float width) { + if (mLineWidth == width) return; + mLineWidth = width; + mGL.glLineWidth(width); + } + + public void setLineSmooth(boolean enabled) { + if (mLineSmooth == enabled) return; + mLineSmooth = enabled; + if (enabled) { + mGL.glEnable(GL11.GL_LINE_SMOOTH); + } else { + mGL.glDisable(GL11.GL_LINE_SMOOTH); + } + } + + public void setTextureAlpha(float alpha) { + if (mTextureAlpha == alpha) return; + mTextureAlpha = alpha; + if (alpha >= OPAQUE_ALPHA) { + // The alpha is need for those texture without alpha channel + mGL.glColor4f(1, 1, 1, 1); + setTexEnvMode(GL11.GL_REPLACE); + } else { + mGL.glColor4f(alpha, alpha, alpha, alpha); + setTexEnvMode(GL11.GL_MODULATE); + } + } + + public void setColorMode(int color, float alpha) { + setBlendEnabled(!Utils.isOpaque(color) || alpha < OPAQUE_ALPHA); + + // Set mTextureAlpha to an invalid value, so that it will reset + // again in setTextureAlpha(float) later. + mTextureAlpha = -1.0f; + + setTexture2DEnabled(false); + + float prealpha = (color >>> 24) * alpha * 65535f / 255f / 255f; + mGL.glColor4x( + Math.round(((color >> 16) & 0xFF) * prealpha), + Math.round(((color >> 8) & 0xFF) * prealpha), + Math.round((color & 0xFF) * prealpha), + Math.round(255 * prealpha)); + } + + public void setTexture2DEnabled(boolean enabled) { + if (mTexture2DEnabled == enabled) return; + mTexture2DEnabled = enabled; + if (enabled) { + mGL.glEnable(GL11.GL_TEXTURE_2D); + } else { + mGL.glDisable(GL11.GL_TEXTURE_2D); + } + } + + public void setBlendEnabled(boolean enabled) { + if (mBlendEnabled == enabled) return; + mBlendEnabled = enabled; + if (enabled) { + mGL.glEnable(GL11.GL_BLEND); + } else { + mGL.glDisable(GL11.GL_BLEND); + } + } + } + + public GL11 getGLInstance() { + return mGL; + } + + public void setCurrentAnimationTimeMillis(long time) { + Utils.assertTrue(time >= 0); + mAnimationTime = time; + } + + public void clearBuffer() { + mGL.glClear(GL10.GL_COLOR_BUFFER_BIT); + } + + private void setTextureCoords(RectF source) { + setTextureCoords(source.left, source.top, source.right, source.bottom); + } + + private void setTextureCoords(float left, float top, + float right, float bottom) { + mGL.glMatrixMode(GL11.GL_TEXTURE); + mTextureMatrixValues[0] = right - left; + mTextureMatrixValues[5] = bottom - top; + mTextureMatrixValues[10] = 1; + mTextureMatrixValues[12] = left; + mTextureMatrixValues[13] = top; + mTextureMatrixValues[15] = 1; + mGL.glLoadMatrixf(mTextureMatrixValues, 0); + mGL.glMatrixMode(GL11.GL_MODELVIEW); + } + + // unloadTexture and deleteBuffer can be called from the finalizer thread, + // so we synchronized on the mUnboundTextures object. + public boolean unloadTexture(BasicTexture t) { + synchronized (mUnboundTextures) { + if (!t.isLoaded(this)) return false; + mUnboundTextures.add(t.mId); + return true; + } + } + + public void deleteBuffer(int bufferId) { + synchronized (mUnboundTextures) { + mDeleteBuffers.add(bufferId); + } + } + + public void deleteRecycledResources() { + synchronized (mUnboundTextures) { + IntArray ids = mUnboundTextures; + if (ids.size() > 0) { + mGL.glDeleteTextures(ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + + ids = mDeleteBuffers; + if (ids.size() > 0) { + mGL.glDeleteBuffers(ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + } + } + + public int save() { + return save(SAVE_FLAG_ALL); + } + + public int save(int saveFlags) { + ConfigState config = obtainRestoreConfig(); + + if ((saveFlags & SAVE_FLAG_ALPHA) != 0) { + config.mAlpha = mAlpha; + } else { + config.mAlpha = -1; + } + + if ((saveFlags & SAVE_FLAG_CLIP) != 0) { + config.mRect.set(mClipRect); + } else { + config.mRect.left = Integer.MAX_VALUE; + } + + if ((saveFlags & SAVE_FLAG_MATRIX) != 0) { + System.arraycopy(mMatrixValues, 0, config.mMatrix, 0, 16); + } else { + config.mMatrix[0] = Float.NEGATIVE_INFINITY; + } + + mRestoreStack.push(config); + return mRestoreStack.size() - 1; + } + + public void restore() { + if (mRestoreStack.isEmpty()) throw new IllegalStateException(); + ConfigState config = mRestoreStack.pop(); + config.restore(this); + freeRestoreConfig(config); + } + + private void freeRestoreConfig(ConfigState action) { + action.mNextFree = mRecycledRestoreAction; + mRecycledRestoreAction = action; + } + + private ConfigState obtainRestoreConfig() { + if (mRecycledRestoreAction != null) { + ConfigState result = mRecycledRestoreAction; + mRecycledRestoreAction = result.mNextFree; + return result; + } + return new ConfigState(); + } + + private static class ConfigState { + float mAlpha; + Rect mRect = new Rect(); + float mMatrix[] = new float[16]; + ConfigState mNextFree; + + public void restore(GLCanvasImpl canvas) { + if (mAlpha >= 0) canvas.setAlpha(mAlpha); + if (mRect.left != Integer.MAX_VALUE) { + Rect rect = mRect; + canvas.mClipRect.set(rect); + canvas.mGL.glScissor( + rect.left, rect.top, rect.width(), rect.height()); + } + if (mMatrix[0] != Float.NEGATIVE_INFINITY) { + System.arraycopy(mMatrix, 0, canvas.mMatrixValues, 0, 16); + } + } + } + + public void dumpStatisticsAndClear() { + String line = String.format( + "MESH:%d, TEX_OES:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d", + mCountDrawMesh, mCountTextureRect, mCountTextureOES, + mCountFillRect, mCountDrawLine); + mCountDrawMesh = 0; + mCountTextureRect = 0; + mCountTextureOES = 0; + mCountFillRect = 0; + mCountDrawLine = 0; + Log.d(TAG, line); + } + + private void saveTransform() { + System.arraycopy(mMatrixValues, 0, mTempMatrix, 0, 16); + } + + private void restoreTransform() { + System.arraycopy(mTempMatrix, 0, mMatrixValues, 0, 16); + } +} diff --git a/src/com/android/gallery3d/ui/GLPaint.java b/src/com/android/gallery3d/ui/GLPaint.java new file mode 100644 index 000000000..9f7b6f1f3 --- /dev/null +++ b/src/com/android/gallery3d/ui/GLPaint.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.common.Utils; + + +public class GLPaint { + public static final int FLAG_ANTI_ALIAS = 0x01; + + private int mFlags = 0; + private float mLineWidth = 1f; + private int mColor = 0; + + public int getFlags() { + return mFlags; + } + + public void setFlags(int flags) { + mFlags = flags; + } + + public void setColor(int color) { + mColor = color; + } + + public int getColor() { + return mColor; + } + + public void setLineWidth(float width) { + Utils.assertTrue(width >= 0); + mLineWidth = width; + } + + public float getLineWidth() { + return mLineWidth; + } + + public void setAntiAlias(boolean enabled) { + if (enabled) { + mFlags |= FLAG_ANTI_ALIAS; + } else { + mFlags &= ~FLAG_ANTI_ALIAS; + } + } + + public boolean getAntiAlias(){ + return (mFlags & FLAG_ANTI_ALIAS) != 0; + } +} diff --git a/src/com/android/gallery3d/ui/GLRoot.java b/src/com/android/gallery3d/ui/GLRoot.java new file mode 100644 index 000000000..24e5794b0 --- /dev/null +++ b/src/com/android/gallery3d/ui/GLRoot.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.anim.CanvasAnimation; + +public interface GLRoot { + + public static interface OnGLIdleListener { + public boolean onGLIdle(GLRoot root, GLCanvas canvas); + } + + public void addOnGLIdleListener(OnGLIdleListener listener); + public void registerLaunchedAnimation(CanvasAnimation animation); + public void requestRender(); + public void requestLayoutContentPane(); + public boolean hasStencil(); + + public void lockRenderThread(); + public void unlockRenderThread(); + + public void setContentPane(GLView content); +} diff --git a/src/com/android/gallery3d/ui/GLRootView.java b/src/com/android/gallery3d/ui/GLRootView.java new file mode 100644 index 000000000..e03adf1c4 --- /dev/null +++ b/src/com/android/gallery3d/ui/GLRootView.java @@ -0,0 +1,414 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.anim.CanvasAnimation; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.GalleryUtils; + +import android.app.Activity; +import android.content.Context; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.opengl.GLSurfaceView; +import android.os.Process; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.MotionEvent; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.concurrent.locks.ReentrantLock; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; + +// The root component of all GLViews. The rendering is done in GL +// thread while the event handling is done in the main thread. To synchronize +// the two threads, the entry points of this package need to synchronize on the +// GLRootView instance unless it can be proved that the rendering +// thread won't access the same thing as the method. The entry points include: +// (1) The public methods of HeadUpDisplay +// (2) The public methods of CameraHeadUpDisplay +// (3) The overridden methods in GLRootView. +public class GLRootView extends GLSurfaceView + implements GLSurfaceView.Renderer, GLRoot { + private static final String TAG = "GLRootView"; + + private static final boolean DEBUG_FPS = false; + private int mFrameCount = 0; + private long mFrameCountingStart = 0; + + private static final boolean DEBUG_INVALIDATE = false; + private int mInvalidateColor = 0; + + private static final boolean DEBUG_DRAWING_STAT = false; + + private static final int FLAG_INITIALIZED = 1; + private static final int FLAG_NEED_LAYOUT = 2; + + private GL11 mGL; + private GLCanvasImpl mCanvas; + + private GLView mContentView; + private DisplayMetrics mDisplayMetrics; + + private int mFlags = FLAG_NEED_LAYOUT; + private volatile boolean mRenderRequested = false; + + private Rect mClipRect = new Rect(); + private int mClipRetryCount = 0; + + private final GalleryEGLConfigChooser mEglConfigChooser = + new GalleryEGLConfigChooser(); + + private final ArrayList mAnimations = + new ArrayList(); + + private final LinkedList mIdleListeners = + new LinkedList(); + + private final IdleRunner mIdleRunner = new IdleRunner(); + + private final ReentrantLock mRenderLock = new ReentrantLock(); + + private static final int TARGET_FRAME_TIME = 33; + private long mLastDrawFinishTime; + private boolean mInDownState = false; + + public GLRootView(Context context) { + this(context, null); + } + + public GLRootView(Context context, AttributeSet attrs) { + super(context, attrs); + mFlags |= FLAG_INITIALIZED; + setBackgroundDrawable(null); + setEGLConfigChooser(mEglConfigChooser); + setRenderer(this); + getHolder().setFormat(PixelFormat.RGB_565); + + // Uncomment this to enable gl error check. + //setDebugFlags(DEBUG_CHECK_GL_ERROR); + } + + public GalleryEGLConfigChooser getEGLConfigChooser() { + return mEglConfigChooser; + } + + @Override + public boolean hasStencil() { + return getEGLConfigChooser().getStencilBits() > 0; + } + + @Override + public void registerLaunchedAnimation(CanvasAnimation animation) { + // Register the newly launched animation so that we can set the start + // time more precisely. (Usually, it takes much longer for first + // rendering, so we set the animation start time as the time we + // complete rendering) + mAnimations.add(animation); + } + + @Override + public void addOnGLIdleListener(OnGLIdleListener listener) { + synchronized (mIdleListeners) { + mIdleListeners.addLast(listener); + mIdleRunner.enable(); + } + } + + @Override + public void setContentPane(GLView content) { + if (mContentView == content) return; + if (mContentView != null) { + if (mInDownState) { + long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain( + now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + mContentView.dispatchTouchEvent(cancelEvent); + cancelEvent.recycle(); + mInDownState = false; + } + mContentView.detachFromRoot(); + BasicTexture.yieldAllTextures(); + } + mContentView = content; + if (content != null) { + content.attachToRoot(this); + requestLayoutContentPane(); + } + } + + public GLView getContentPane() { + return mContentView; + } + + @Override + public void requestRender() { + if (DEBUG_INVALIDATE) { + StackTraceElement e = Thread.currentThread().getStackTrace()[4]; + String caller = e.getFileName() + ":" + e.getLineNumber() + " "; + Log.d(TAG, "invalidate: " + caller); + } + if (mRenderRequested) return; + mRenderRequested = true; + super.requestRender(); + } + + @Override + public void requestLayoutContentPane() { + mRenderLock.lock(); + try { + if (mContentView == null || (mFlags & FLAG_NEED_LAYOUT) != 0) return; + + // "View" system will invoke onLayout() for initialization(bug ?), we + // have to ignore it since the GLThread is not ready yet. + if ((mFlags & FLAG_INITIALIZED) == 0) return; + + mFlags |= FLAG_NEED_LAYOUT; + requestRender(); + } finally { + mRenderLock.unlock(); + } + } + + private void layoutContentPane() { + mFlags &= ~FLAG_NEED_LAYOUT; + int width = getWidth(); + int height = getHeight(); + Log.i(TAG, "layout content pane " + width + "x" + height); + if (mContentView != null && width != 0 && height != 0) { + mContentView.layout(0, 0, width, height); + } + // Uncomment this to dump the view hierarchy. + //mContentView.dumpTree(""); + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + if (changed) requestLayoutContentPane(); + } + + /** + * Called when the context is created, possibly after automatic destruction. + */ + // This is a GLSurfaceView.Renderer callback + @Override + public void onSurfaceCreated(GL10 gl1, EGLConfig config) { + GL11 gl = (GL11) gl1; + if (mGL != null) { + // The GL Object has changed + Log.i(TAG, "GLObject has changed from " + mGL + " to " + gl); + } + mGL = gl; + mCanvas = new GLCanvasImpl(gl); + if (!DEBUG_FPS) { + setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + } else { + setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); + } + } + + /** + * Called when the OpenGL surface is recreated without destroying the + * context. + */ + // This is a GLSurfaceView.Renderer callback + @Override + public void onSurfaceChanged(GL10 gl1, int width, int height) { + Log.i(TAG, "onSurfaceChanged: " + width + "x" + height + + ", gl10: " + gl1.toString()); + Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY); + GalleryUtils.setRenderThread(); + GL11 gl = (GL11) gl1; + Utils.assertTrue(mGL == gl); + + mCanvas.setSize(width, height); + + mClipRect.set(0, 0, width, height); + mClipRetryCount = 2; + } + + private void outputFps() { + long now = System.nanoTime(); + if (mFrameCountingStart == 0) { + mFrameCountingStart = now; + } else if ((now - mFrameCountingStart) > 1000000000) { + Log.d(TAG, "fps: " + (double) mFrameCount + * 1000000000 / (now - mFrameCountingStart)); + mFrameCountingStart = now; + mFrameCount = 0; + } + ++mFrameCount; + } + + @Override + public void onDrawFrame(GL10 gl) { + mRenderLock.lock(); + try { + onDrawFrameLocked(gl); + } finally { + mRenderLock.unlock(); + } + long end = SystemClock.uptimeMillis(); + + if (mLastDrawFinishTime != 0) { + long wait = mLastDrawFinishTime + TARGET_FRAME_TIME - end; + if (wait > 0) { + SystemClock.sleep(wait); + } + } + mLastDrawFinishTime = SystemClock.uptimeMillis(); + } + + private void onDrawFrameLocked(GL10 gl) { + if (DEBUG_FPS) outputFps(); + + // release the unbound textures and deleted buffers. + mCanvas.deleteRecycledResources(); + + // reset texture upload limit + UploadedTexture.resetUploadLimit(); + + mRenderRequested = false; + + if ((mFlags & FLAG_NEED_LAYOUT) != 0) layoutContentPane(); + + // OpenGL seems having a bug causing us not being able to reset the + // scissor box in "onSurfaceChanged()". We have to do it in the second + // onDrawFrame(). + if (mClipRetryCount > 0) { + --mClipRetryCount; + Rect clip = mClipRect; + gl.glScissor(clip.left, clip.top, clip.width(), clip.height()); + } + + mCanvas.setCurrentAnimationTimeMillis(SystemClock.uptimeMillis()); + if (mContentView != null) { + mContentView.render(mCanvas); + } + + if (!mAnimations.isEmpty()) { + long now = SystemClock.uptimeMillis(); + for (int i = 0, n = mAnimations.size(); i < n; i++) { + mAnimations.get(i).setStartTime(now); + } + mAnimations.clear(); + } + + if (UploadedTexture.uploadLimitReached()) { + requestRender(); + } + + synchronized (mIdleListeners) { + if (!mRenderRequested && !mIdleListeners.isEmpty()) { + mIdleRunner.enable(); + } + } + + if (DEBUG_INVALIDATE) { + mCanvas.fillRect(10, 10, 5, 5, mInvalidateColor); + mInvalidateColor = ~mInvalidateColor; + } + + if (DEBUG_DRAWING_STAT) { + mCanvas.dumpStatisticsAndClear(); + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + int action = event.getAction(); + if (action == MotionEvent.ACTION_CANCEL + || action == MotionEvent.ACTION_UP) { + mInDownState = false; + } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) { + return false; + } + mRenderLock.lock(); + try { + // If this has been detached from root, we don't need to handle event + boolean handled = mContentView != null + && mContentView.dispatchTouchEvent(event); + if (action == MotionEvent.ACTION_DOWN && handled) { + mInDownState = true; + } + return handled; + } finally { + mRenderLock.unlock(); + } + } + + public DisplayMetrics getDisplayMetrics() { + if (mDisplayMetrics == null) { + mDisplayMetrics = new DisplayMetrics(); + ((Activity) getContext()).getWindowManager() + .getDefaultDisplay().getMetrics(mDisplayMetrics); + } + return mDisplayMetrics; + } + + public GLCanvas getCanvas() { + return mCanvas; + } + + private class IdleRunner implements Runnable { + // true if the idle runner is in the queue + private boolean mActive = false; + + @Override + public void run() { + OnGLIdleListener listener; + synchronized (mIdleListeners) { + mActive = false; + if (mRenderRequested) return; + if (mIdleListeners.isEmpty()) return; + listener = mIdleListeners.removeFirst(); + } + mRenderLock.lock(); + try { + if (!listener.onGLIdle(GLRootView.this, mCanvas)) return; + } finally { + mRenderLock.unlock(); + } + synchronized (mIdleListeners) { + mIdleListeners.addLast(listener); + enable(); + } + } + + public void enable() { + // Who gets the flag can add it to the queue + if (mActive) return; + mActive = true; + queueEvent(this); + } + } + + @Override + public void lockRenderThread() { + mRenderLock.lock(); + } + + @Override + public void unlockRenderThread() { + mRenderLock.unlock(); + } +} diff --git a/src/com/android/gallery3d/ui/GLView.java b/src/com/android/gallery3d/ui/GLView.java new file mode 100644 index 000000000..c59327831 --- /dev/null +++ b/src/com/android/gallery3d/ui/GLView.java @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.anim.CanvasAnimation; +import com.android.gallery3d.common.Utils; + +import android.graphics.Rect; +import android.os.SystemClock; +import android.view.MotionEvent; + +import java.util.ArrayList; + +// GLView is a UI component. It can render to a GLCanvas and accept touch +// events. A GLView may have zero or more child GLView and they form a tree +// structure. The rendering and event handling will pass through the tree +// structure. +// +// A GLView tree should be attached to a GLRoot before event dispatching and +// rendering happens. GLView asks GLRoot to re-render or re-layout the +// GLView hierarchy using requestRender() and requestLayoutContentPane(). +// +// The render() method is called in a separate thread. Before calling +// dispatchTouchEvent() and layout(), GLRoot acquires a lock to avoid the +// rendering thread running at the same time. If there are other entry points +// from main thread (like a Handler) in your GLView, you need to call +// lockRendering() if the rendering thread should not run at the same time. +// +public class GLView { + private static final String TAG = "GLView"; + + public static final int VISIBLE = 0; + public static final int INVISIBLE = 1; + + private static final int FLAG_INVISIBLE = 1; + private static final int FLAG_SET_MEASURED_SIZE = 2; + private static final int FLAG_LAYOUT_REQUESTED = 4; + + protected final Rect mBounds = new Rect(); + protected final Rect mPaddings = new Rect(); + + private GLRoot mRoot; + protected GLView mParent; + private ArrayList mComponents; + private GLView mMotionTarget; + + private CanvasAnimation mAnimation; + + private int mViewFlags = 0; + + protected int mMeasuredWidth = 0; + protected int mMeasuredHeight = 0; + + private int mLastWidthSpec = -1; + private int mLastHeightSpec = -1; + + protected int mScrollY = 0; + protected int mScrollX = 0; + protected int mScrollHeight = 0; + protected int mScrollWidth = 0; + + public void startAnimation(CanvasAnimation animation) { + GLRoot root = getGLRoot(); + if (root == null) throw new IllegalStateException(); + + mAnimation = animation; + mAnimation.start(); + root.registerLaunchedAnimation(mAnimation); + invalidate(); + } + + // Sets the visiblity of this GLView (either GLView.VISIBLE or + // GLView.INVISIBLE). + public void setVisibility(int visibility) { + if (visibility == getVisibility()) return; + if (visibility == VISIBLE) { + mViewFlags &= ~FLAG_INVISIBLE; + } else { + mViewFlags |= FLAG_INVISIBLE; + } + onVisibilityChanged(visibility); + invalidate(); + } + + // Returns GLView.VISIBLE or GLView.INVISIBLE + public int getVisibility() { + return (mViewFlags & FLAG_INVISIBLE) == 0 ? VISIBLE : INVISIBLE; + } + + // This should only be called on the content pane (the topmost GLView). + public void attachToRoot(GLRoot root) { + Utils.assertTrue(mParent == null && mRoot == null); + onAttachToRoot(root); + } + + // This should only be called on the content pane (the topmost GLView). + public void detachFromRoot() { + Utils.assertTrue(mParent == null && mRoot != null); + onDetachFromRoot(); + } + + // Returns the number of children of the GLView. + public int getComponentCount() { + return mComponents == null ? 0 : mComponents.size(); + } + + // Returns the children for the given index. + public GLView getComponent(int index) { + if (mComponents == null) { + throw new ArrayIndexOutOfBoundsException(index); + } + return mComponents.get(index); + } + + // Adds a child to this GLView. + public void addComponent(GLView component) { + // Make sure the component doesn't have a parent currently. + if (component.mParent != null) throw new IllegalStateException(); + + // Build parent-child links + if (mComponents == null) { + mComponents = new ArrayList(); + } + mComponents.add(component); + component.mParent = this; + + // If this is added after we have a root, tell the component. + if (mRoot != null) { + component.onAttachToRoot(mRoot); + } + } + + // Removes a child from this GLView. + public boolean removeComponent(GLView component) { + if (mComponents == null) return false; + if (mComponents.remove(component)) { + removeOneComponent(component); + return true; + } + return false; + } + + // Removes all children of this GLView. + public void removeAllComponents() { + for (int i = 0, n = mComponents.size(); i < n; ++i) { + removeOneComponent(mComponents.get(i)); + } + mComponents.clear(); + } + + private void removeOneComponent(GLView component) { + if (mMotionTarget == component) { + long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain( + now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + dispatchTouchEvent(cancelEvent); + cancelEvent.recycle(); + } + component.onDetachFromRoot(); + component.mParent = null; + } + + public Rect bounds() { + return mBounds; + } + + public int getWidth() { + return mBounds.right - mBounds.left; + } + + public int getHeight() { + return mBounds.bottom - mBounds.top; + } + + public GLRoot getGLRoot() { + return mRoot; + } + + // Request re-rendering of the view hierarchy. + // This is used for animation or when the contents changed. + public void invalidate() { + GLRoot root = getGLRoot(); + if (root != null) root.requestRender(); + } + + // Request re-layout of the view hierarchy. + public void requestLayout() { + mViewFlags |= FLAG_LAYOUT_REQUESTED; + mLastHeightSpec = -1; + mLastWidthSpec = -1; + if (mParent != null) { + mParent.requestLayout(); + } else { + // Is this a content pane ? + GLRoot root = getGLRoot(); + if (root != null) root.requestLayoutContentPane(); + } + } + + protected void render(GLCanvas canvas) { + renderBackground(canvas); + for (int i = 0, n = getComponentCount(); i < n; ++i) { + renderChild(canvas, getComponent(i)); + } + } + + protected void renderBackground(GLCanvas view) { + } + + protected void renderChild(GLCanvas canvas, GLView component) { + if (component.getVisibility() != GLView.VISIBLE + && component.mAnimation == null) return; + + int xoffset = component.mBounds.left - mScrollX; + int yoffset = component.mBounds.top - mScrollY; + + canvas.translate(xoffset, yoffset, 0); + + CanvasAnimation anim = component.mAnimation; + if (anim != null) { + canvas.save(anim.getCanvasSaveFlags()); + if (anim.calculate(canvas.currentAnimationTimeMillis())) { + invalidate(); + } else { + component.mAnimation = null; + } + anim.apply(canvas); + } + component.render(canvas); + if (anim != null) canvas.restore(); + canvas.translate(-xoffset, -yoffset, 0); + } + + protected boolean onTouch(MotionEvent event) { + return false; + } + + protected boolean dispatchTouchEvent(MotionEvent event, + int x, int y, GLView component, boolean checkBounds) { + Rect rect = component.mBounds; + int left = rect.left; + int top = rect.top; + if (!checkBounds || rect.contains(x, y)) { + event.offsetLocation(-left, -top); + if (component.dispatchTouchEvent(event)) { + event.offsetLocation(left, top); + return true; + } + event.offsetLocation(left, top); + } + return false; + } + + protected boolean dispatchTouchEvent(MotionEvent event) { + int x = (int) event.getX(); + int y = (int) event.getY(); + int action = event.getAction(); + if (mMotionTarget != null) { + if (action == MotionEvent.ACTION_DOWN) { + MotionEvent cancel = MotionEvent.obtain(event); + cancel.setAction(MotionEvent.ACTION_CANCEL); + dispatchTouchEvent(cancel, x, y, mMotionTarget, false); + mMotionTarget = null; + } else { + dispatchTouchEvent(event, x, y, mMotionTarget, false); + if (action == MotionEvent.ACTION_CANCEL + || action == MotionEvent.ACTION_UP) { + mMotionTarget = null; + } + return true; + } + } + if (action == MotionEvent.ACTION_DOWN) { + // in the reverse rendering order + for (int i = getComponentCount() - 1; i >= 0; --i) { + GLView component = getComponent(i); + if (component.getVisibility() != GLView.VISIBLE) continue; + if (dispatchTouchEvent(event, x, y, component, true)) { + mMotionTarget = component; + return true; + } + } + } + return onTouch(event); + } + + public Rect getPaddings() { + return mPaddings; + } + + public void setPaddings(Rect paddings) { + mPaddings.set(paddings); + } + + public void setPaddings(int left, int top, int right, int bottom) { + mPaddings.set(left, top, right, bottom); + } + + public void layout(int left, int top, int right, int bottom) { + boolean sizeChanged = setBounds(left, top, right, bottom); + if (sizeChanged) { + mViewFlags &= ~FLAG_LAYOUT_REQUESTED; + onLayout(true, left, top, right, bottom); + } else if ((mViewFlags & FLAG_LAYOUT_REQUESTED)!= 0) { + mViewFlags &= ~FLAG_LAYOUT_REQUESTED; + onLayout(false, left, top, right, bottom); + } + } + + private boolean setBounds(int left, int top, int right, int bottom) { + boolean sizeChanged = (right - left) != (mBounds.right - mBounds.left) + || (bottom - top) != (mBounds.bottom - mBounds.top); + mBounds.set(left, top, right, bottom); + return sizeChanged; + } + + public void measure(int widthSpec, int heightSpec) { + if (widthSpec == mLastWidthSpec && heightSpec == mLastHeightSpec + && (mViewFlags & FLAG_LAYOUT_REQUESTED) == 0) { + return; + } + + mLastWidthSpec = widthSpec; + mLastHeightSpec = heightSpec; + + mViewFlags &= ~FLAG_SET_MEASURED_SIZE; + onMeasure(widthSpec, heightSpec); + if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) { + throw new IllegalStateException(getClass().getName() + + " should call setMeasuredSize() in onMeasure()"); + } + } + + protected void onMeasure(int widthSpec, int heightSpec) { + } + + protected void setMeasuredSize(int width, int height) { + mViewFlags |= FLAG_SET_MEASURED_SIZE; + mMeasuredWidth = width; + mMeasuredHeight = height; + } + + public int getMeasuredWidth() { + return mMeasuredWidth; + } + + public int getMeasuredHeight() { + return mMeasuredHeight; + } + + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + } + + /** + * Gets the bounds of the given descendant that relative to this view. + */ + public boolean getBoundsOf(GLView descendant, Rect out) { + int xoffset = 0; + int yoffset = 0; + GLView view = descendant; + while (view != this) { + if (view == null) return false; + Rect bounds = view.mBounds; + xoffset += bounds.left; + yoffset += bounds.top; + view = view.mParent; + } + out.set(xoffset, yoffset, xoffset + descendant.getWidth(), + yoffset + descendant.getHeight()); + return true; + } + + protected void onVisibilityChanged(int visibility) { + for (int i = 0, n = getComponentCount(); i < n; ++i) { + GLView child = getComponent(i); + if (child.getVisibility() == GLView.VISIBLE) { + child.onVisibilityChanged(visibility); + } + } + } + + protected void onAttachToRoot(GLRoot root) { + mRoot = root; + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).onAttachToRoot(root); + } + } + + protected void onDetachFromRoot() { + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).onDetachFromRoot(); + } + mRoot = null; + } + + public void lockRendering() { + if (mRoot != null) { + mRoot.lockRenderThread(); + } + } + + public void unlockRendering() { + if (mRoot != null) { + mRoot.unlockRenderThread(); + } + } + + // This is for debugging only. + // Dump the view hierarchy into log. + void dumpTree(String prefix) { + Log.d(TAG, prefix + getClass().getSimpleName()); + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).dumpTree(prefix + "...."); + } + } +} diff --git a/src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java b/src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java new file mode 100644 index 000000000..1d50d43f7 --- /dev/null +++ b/src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.gallery3d.ui; + +import android.opengl.GLSurfaceView.EGLConfigChooser; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLDisplay; + +/* + * The code is copied/adapted from + * android.opengl.GLSurfaceView.BaseConfigChooser. Here we try to + * choose a configuration that support RGBA_8888 format and if possible, + * with stencil buffer, but is not required. + */ +class GalleryEGLConfigChooser implements EGLConfigChooser { + + private static final String TAG = "GalleryEGLConfigChooser"; + private int mStencilBits; + + private final int mConfigSpec[] = new int[] { + EGL10.EGL_RED_SIZE, 5, + EGL10.EGL_GREEN_SIZE, 6, + EGL10.EGL_BLUE_SIZE, 5, + EGL10.EGL_ALPHA_SIZE, 0, + EGL10.EGL_NONE + }; + + public int getStencilBits() { + return mStencilBits; + } + + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + int[] numConfig = new int[1]; + if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, numConfig)) { + throw new RuntimeException("eglChooseConfig failed"); + } + + if (numConfig[0] <= 0) { + throw new RuntimeException("No configs match configSpec"); + } + + EGLConfig[] configs = new EGLConfig[numConfig[0]]; + if (!egl.eglChooseConfig(display, + mConfigSpec, configs, configs.length, numConfig)) { + throw new RuntimeException(); + } + + return chooseConfig(egl, display, configs); + } + + private EGLConfig chooseConfig( + EGL10 egl, EGLDisplay display, EGLConfig configs[]) { + + EGLConfig result = null; + int minStencil = Integer.MAX_VALUE; + int value[] = new int[1]; + + // Because we need only one bit of stencil, try to choose a config that + // has stencil support but with smallest number of stencil bits. If + // none is found, choose any one. + for (int i = 0, n = configs.length; i < n; ++i) { + if (egl.eglGetConfigAttrib( + display, configs[i], EGL10.EGL_RED_SIZE, value)) { + // Filter out ARGB 8888 configs. + if (value[0] == 8) continue; + } + if (egl.eglGetConfigAttrib( + display, configs[i], EGL10.EGL_STENCIL_SIZE, value)) { + if (value[0] == 0) continue; + if (value[0] < minStencil) { + minStencil = value[0]; + result = configs[i]; + } + } else { + throw new RuntimeException( + "eglGetConfigAttrib error: " + egl.eglGetError()); + } + } + if (result == null) result = configs[0]; + egl.eglGetConfigAttrib( + display, result, EGL10.EGL_STENCIL_SIZE, value); + mStencilBits = value[0]; + logConfig(egl, display, result); + return result; + } + + private static final int[] ATTR_ID = { + EGL10.EGL_RED_SIZE, + EGL10.EGL_GREEN_SIZE, + EGL10.EGL_BLUE_SIZE, + EGL10.EGL_ALPHA_SIZE, + EGL10.EGL_DEPTH_SIZE, + EGL10.EGL_STENCIL_SIZE, + EGL10.EGL_CONFIG_ID, + EGL10.EGL_CONFIG_CAVEAT + }; + + private static final String[] ATTR_NAME = { + "R", "G", "B", "A", "D", "S", "ID", "CAVEAT" + }; + + private void logConfig(EGL10 egl, EGLDisplay display, EGLConfig config) { + int value[] = new int[1]; + StringBuilder sb = new StringBuilder(); + for (int j = 0; j < ATTR_ID.length; j++) { + egl.eglGetConfigAttrib(display, config, ATTR_ID[j], value); + sb.append(ATTR_NAME[j] + value[0] + " "); + } + Log.i(TAG, "Config chosen: " + sb.toString()); + } +} diff --git a/src/com/android/gallery3d/ui/GridDrawer.java b/src/com/android/gallery3d/ui/GridDrawer.java new file mode 100644 index 000000000..54b175cb4 --- /dev/null +++ b/src/com/android/gallery3d/ui/GridDrawer.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.Path; + +import android.content.Context; +import android.graphics.Color; + +public class GridDrawer extends IconDrawer { + private final NinePatchTexture mFrame; + private final NinePatchTexture mFrameSelected; + private final NinePatchTexture mFrameSelectedTop; + private final NinePatchTexture mImportBackground; + private Texture mImportLabel; + private int mGridWidth; + private final SelectionManager mSelectionManager; + private final Context mContext; + private final int FONT_SIZE = 14; + private final int FONT_COLOR = Color.WHITE; + private final int IMPORT_LABEL_PADDING = 10; + private boolean mSelectionMode; + + public GridDrawer(Context context, SelectionManager selectionManager) { + super(context); + mContext = context; + mFrame = new NinePatchTexture(context, R.drawable.album_frame); + mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected); + mFrameSelectedTop = new NinePatchTexture(context, R.drawable.grid_selected_top); + mImportBackground = new NinePatchTexture(context, R.drawable.import_translucent); + mSelectionManager = selectionManager; + } + + @Override + public void prepareDrawing() { + mSelectionMode = mSelectionManager.inSelectionMode(); + } + + @Override + public void draw(GLCanvas canvas, Texture content, int width, int height, + int rotation, Path path, int topIndex, int dataSourceType, + int mediaType, boolean wantCache, boolean isCaching) { + + int x = -width / 2; + int y = -height / 2; + + drawWithRotationAndGray(canvas, content, x, y, width, height, rotation, + topIndex); + + if (((rotation / 90) & 0x01) == 1) { + int temp = width; + width = height; + height = temp; + x = -width / 2; + y = -height / 2; + } + + drawVideoOverlay(canvas, mediaType, x, y, width, height, topIndex); + + NinePatchTexture frame; + if (mSelectionMode && mSelectionManager.isItemSelected(path)) { + frame = topIndex == 0 ? mFrameSelectedTop : mFrameSelected; + } else { + frame = mFrame; + } + + drawFrame(canvas, frame, x, y, width, height); + + if (topIndex == 0) { + ResourceTexture icon = getIcon(dataSourceType); + if (icon != null) { + IconDimension id = getIconDimension(icon, width, height); + if (dataSourceType == DATASOURCE_TYPE_MTP) { + if (mImportLabel == null || mGridWidth != width) { + mGridWidth = width; + mImportLabel = MultiLineTexture.newInstance( + mContext.getString(R.string.click_import), + width - id.width - IMPORT_LABEL_PADDING, FONT_SIZE, FONT_COLOR); + } + int bgHeight = Math.max(id.height, mImportLabel.getHeight()); + mImportBackground.setSize(width, bgHeight); + mImportBackground.draw(canvas, x, -y - bgHeight); + mImportLabel.draw(canvas, x + id.width + IMPORT_LABEL_PADDING, + -y - bgHeight + Math.abs(bgHeight - mImportLabel.getHeight()) / 2); + } + icon.draw(canvas, id.x, id.y, id.width, id.height); + } + } + } + + @Override + public void drawFocus(GLCanvas canvas, int width, int height) { + } +} diff --git a/src/com/android/gallery3d/ui/HighlightDrawer.java b/src/com/android/gallery3d/ui/HighlightDrawer.java new file mode 100644 index 000000000..9d5868bcb --- /dev/null +++ b/src/com/android/gallery3d/ui/HighlightDrawer.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.Path; + +import android.content.Context; + +public class HighlightDrawer extends IconDrawer { + private final NinePatchTexture mFrame; + private final NinePatchTexture mFrameSelected; + private final NinePatchTexture mFrameSelectedTop; + private SelectionManager mSelectionManager; + private Path mHighlightItem; + + public HighlightDrawer(Context context) { + super(context); + mFrame = new NinePatchTexture(context, R.drawable.album_frame); + mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected); + mFrameSelectedTop = new NinePatchTexture(context, R.drawable.grid_selected_top); + } + + public void setHighlightItem(Path item) { + mHighlightItem = item; + } + + public void draw(GLCanvas canvas, Texture content, int width, int height, + int rotation, Path path, int topIndex, int dataSourceType, + int mediaType, boolean wantCache, boolean isCaching) { + int x = -width / 2; + int y = -height / 2; + + drawWithRotationAndGray(canvas, content, x, y, width, height, rotation, + topIndex); + + if (((rotation / 90) & 0x01) == 1) { + int temp = width; + width = height; + height = temp; + x = -width / 2; + y = -height / 2; + } + + drawVideoOverlay(canvas, mediaType, x, y, width, height, topIndex); + + NinePatchTexture frame; + if (path == mHighlightItem) { + frame = topIndex == 0 ? mFrameSelectedTop : mFrameSelected; + } else { + frame = mFrame; + } + + drawFrame(canvas, frame, x, y, width, height); + + if (topIndex == 0) { + drawIcon(canvas, width, height, dataSourceType); + } + } +} diff --git a/src/com/android/gallery3d/ui/Icon.java b/src/com/android/gallery3d/ui/Icon.java new file mode 100644 index 000000000..c710859f8 --- /dev/null +++ b/src/com/android/gallery3d/ui/Icon.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.graphics.Rect; + +public class Icon extends GLView { + private final BasicTexture mIcon; + + // The width and height requested by the user. + private int mReqWidth; + private int mReqHeight; + + public Icon(Context context, int iconId, int width, int height) { + this(context, new ResourceTexture(context, iconId), width, height); + } + + public Icon(Context context, BasicTexture icon, int width, int height) { + mIcon = icon; + mReqWidth = width; + mReqHeight = height; + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + MeasureHelper.getInstance(this) + .setPreferredContentSize(mReqWidth, mReqHeight) + .measure(widthSpec, heightSpec); + } + + @Override + protected void render(GLCanvas canvas) { + Rect p = mPaddings; + + int width = getWidth() - p.left - p.right; + int height = getHeight() - p.top - p.bottom; + + // Draw the icon in the center of the space + int xoffset = p.left + (width - mReqWidth) / 2; + int yoffset = p.top + (height - mReqHeight) / 2; + + mIcon.draw(canvas, xoffset, yoffset, mReqWidth, mReqHeight); + } +} diff --git a/src/com/android/gallery3d/ui/IconDrawer.java b/src/com/android/gallery3d/ui/IconDrawer.java new file mode 100644 index 000000000..91732d338 --- /dev/null +++ b/src/com/android/gallery3d/ui/IconDrawer.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.MediaObject; + +import android.content.Context; + +public abstract class IconDrawer extends SelectionDrawer { + private final String TAG = "IconDrawer"; + private final ResourceTexture mLocalSetIcon; + private final ResourceTexture mCameraIcon; + private final ResourceTexture mPicasaIcon; + private final ResourceTexture mMtpIcon; + private final Texture mVideoOverlay; + private final Texture mVideoPlayIcon; + + public static class IconDimension { + int x; + int y; + int width; + int height; + } + + public IconDrawer(Context context) { + mLocalSetIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_folder_holo); + mCameraIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_camera_holo); + mPicasaIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_picassa_holo); + mMtpIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_ptp_holo); + mVideoOverlay = new ResourceTexture(context, + R.drawable.thumbnail_album_video_overlay_holo); + mVideoPlayIcon = new ResourceTexture(context, + R.drawable.videooverlay); + } + + @Override + public void prepareDrawing() { + } + + protected IconDimension drawIcon(GLCanvas canvas, int width, int height, + int dataSourceType) { + ResourceTexture icon = getIcon(dataSourceType); + + if (icon != null) { + IconDimension id = getIconDimension(icon, width, height); + icon.draw(canvas, id.x, id.y, id.width, id.height); + return id; + } + return null; + } + + protected ResourceTexture getIcon(int dataSourceType) { + ResourceTexture icon = null; + switch (dataSourceType) { + case DATASOURCE_TYPE_LOCAL: + icon = mLocalSetIcon; + break; + case DATASOURCE_TYPE_PICASA: + icon = mPicasaIcon; + break; + case DATASOURCE_TYPE_CAMERA: + icon = mCameraIcon; + break; + case DATASOURCE_TYPE_MTP: + icon = mMtpIcon; + break; + default: + break; + } + + return icon; + } + + protected IconDimension getIconDimension(ResourceTexture icon, int width, + int height) { + IconDimension id = new IconDimension(); + float scale = 0.25f * width / icon.getWidth(); + id.width = (int) (scale * icon.getWidth()); + id.height = (int) (scale * icon.getHeight()); + id.x = -width / 2; + id.y = height / 2 - id.height; + return id; + } + + protected void drawVideoOverlay(GLCanvas canvas, int mediaType, + int x, int y, int width, int height, int topIndex) { + if (mediaType != MediaObject.MEDIA_TYPE_VIDEO) return; + mVideoOverlay.draw(canvas, x, y, width, height); + if (topIndex == 0) { + int side = Math.min(width, height) / 6; + mVideoPlayIcon.draw(canvas, -side / 2, -side / 2, side, side); + } + } + + @Override + public void drawFocus(GLCanvas canvas, int width, int height) { + } +} diff --git a/src/com/android/gallery3d/ui/ImportCompleteListener.java b/src/com/android/gallery3d/ui/ImportCompleteListener.java new file mode 100644 index 000000000..5c52ea135 --- /dev/null +++ b/src/com/android/gallery3d/ui/ImportCompleteListener.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AlbumPage; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.util.MediaSetUtils; + +import android.content.Context; +import android.os.Bundle; +import android.widget.Toast; + +public class ImportCompleteListener implements MenuExecutor.ProgressListener { + private GalleryActivity mActivity; + + public ImportCompleteListener(GalleryActivity galleryActivity) { + mActivity = galleryActivity; + } + + public void onProgressComplete(int result) { + int message; + if (result == MenuExecutor.EXECUTION_RESULT_SUCCESS) { + message = R.string.import_complete; + goToImportedAlbum(); + } else { + message = R.string.import_fail; + } + Toast.makeText(mActivity.getAndroidContext(), message, Toast.LENGTH_LONG).show(); + } + + public void onProgressUpdate(int index) { + } + + private void goToImportedAlbum() { + String pathOfImportedAlbum = "/local/all/" + MediaSetUtils.IMPORTED_BUCKET_ID; + Bundle data = new Bundle(); + data.putString(AlbumPage.KEY_MEDIA_PATH, pathOfImportedAlbum); + mActivity.getStateManager().startState(AlbumPage.class, data); + } + +} diff --git a/src/com/android/gallery3d/ui/Label.java b/src/com/android/gallery3d/ui/Label.java new file mode 100644 index 000000000..6a70a1895 --- /dev/null +++ b/src/com/android/gallery3d/ui/Label.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; + +public class Label extends GLView { + private static final String TAG = "Label"; + public static final int NULL_ID = 0; + + private static final int FONT_SIZE = 18; + private static final int FONT_COLOR = Color.WHITE; + + private String mText; + private StringTexture mTexture; + private int mFontSize, mFontColor; + + public Label(Context context, int stringId, + int fontSize, int fontColor) { + this(context, context.getString(stringId), fontSize, fontColor); + } + + public Label(Context context, int stringId) { + this(context, stringId, FONT_SIZE, FONT_COLOR); + } + + public Label(Context context, String text) { + this(context, text, FONT_SIZE, FONT_COLOR); + } + + public Label(Context context, String text, int fontSize, int fontColor) { + //TODO: cut the text if it is too long + mText = text; + mTexture = StringTexture.newInstance(text, fontSize, fontColor); + mFontSize = fontSize; + mFontColor = fontColor; + } + + public void setText(String text) { + if (!mText.equals(text)) { + mText = text; + mTexture = StringTexture.newInstance(text, mFontSize, mFontColor); + requestLayout(); + } + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + int width = mTexture.getWidth(); + int height = mTexture.getHeight(); + MeasureHelper.getInstance(this) + .setPreferredContentSize(width, height) + .measure(widthSpec, heightSpec); + } + + @Override + protected void render(GLCanvas canvas) { + Rect p = mPaddings; + + int width = getWidth() - p.left - p.right; + int height = getHeight() - p.top - p.bottom; + + int xoffset = p.left + (width - mTexture.getWidth()) / 2; + int yoffset = p.top + (height - mTexture.getHeight()) / 2; + + mTexture.draw(canvas, xoffset, yoffset); + } +} diff --git a/src/com/android/gallery3d/ui/Log.java b/src/com/android/gallery3d/ui/Log.java new file mode 100644 index 000000000..32adc98eb --- /dev/null +++ b/src/com/android/gallery3d/ui/Log.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +public class Log { + public static int v(String tag, String msg) { + return android.util.Log.v(tag, msg); + } + public static int v(String tag, String msg, Throwable tr) { + return android.util.Log.v(tag, msg, tr); + } + public static int d(String tag, String msg) { + return android.util.Log.d(tag, msg); + } + public static int d(String tag, String msg, Throwable tr) { + return android.util.Log.d(tag, msg, tr); + } + public static int i(String tag, String msg) { + return android.util.Log.i(tag, msg); + } + public static int i(String tag, String msg, Throwable tr) { + return android.util.Log.i(tag, msg, tr); + } + public static int w(String tag, String msg) { + return android.util.Log.w(tag, msg); + } + public static int w(String tag, String msg, Throwable tr) { + return android.util.Log.w(tag, msg, tr); + } + public static int w(String tag, Throwable tr) { + return android.util.Log.w(tag, tr); + } + public static int e(String tag, String msg) { + return android.util.Log.e(tag, msg); + } + public static int e(String tag, String msg, Throwable tr) { + return android.util.Log.e(tag, msg, tr); + } +} diff --git a/src/com/android/gallery3d/ui/ManageCacheDrawer.java b/src/com/android/gallery3d/ui/ManageCacheDrawer.java new file mode 100644 index 000000000..cf1e39e24 --- /dev/null +++ b/src/com/android/gallery3d/ui/ManageCacheDrawer.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.Path; + +import android.content.Context; + +public class ManageCacheDrawer extends IconDrawer { + private static final int COLOR_CACHING_BACKGROUND = 0x7F000000; + private static final int ICON_SIZE = 36; + private final NinePatchTexture mFrame; + private final ResourceTexture mCheckedItem; + private final ResourceTexture mUnCheckedItem; + private final SelectionManager mSelectionManager; + + private final ResourceTexture mLocalAlbumIcon; + private final StringTexture mCaching; + + public ManageCacheDrawer(Context context, SelectionManager selectionManager) { + super(context); + mFrame = new NinePatchTexture(context, R.drawable.manage_frame); + mCheckedItem = new ResourceTexture(context, R.drawable.btn_make_offline_normal_on_holo_dark); + mUnCheckedItem = new ResourceTexture(context, R.drawable.btn_make_offline_normal_off_holo_dark); + mLocalAlbumIcon = new ResourceTexture(context, R.drawable.btn_make_offline_disabled_on_holo_dark); + String cachingLabel = context.getString(R.string.caching_label); + mCaching = StringTexture.newInstance(cachingLabel, 12, 0xffffffff); + mSelectionManager = selectionManager; + } + + @Override + public void prepareDrawing() { + } + + private static boolean isLocal(int dataSourceType) { + return dataSourceType != DATASOURCE_TYPE_PICASA; + } + + @Override + public void draw(GLCanvas canvas, Texture content, int width, int height, + int rotation, Path path, int topIndex, int dataSourceType, + int mediaType, boolean wantCache, boolean isCaching) { + + boolean selected = mSelectionManager.isItemSelected(path); + boolean chooseToCache = wantCache ^ selected; + + int x = -width / 2; + int y = -height / 2; + + drawWithRotationAndGray(canvas, content, x, y, width, height, rotation, + topIndex); + + if (((rotation / 90) & 0x01) == 1) { + int temp = width; + width = height; + height = temp; + x = -width / 2; + y = -height / 2; + } + + drawVideoOverlay(canvas, mediaType, x, y, width, height, topIndex); + + drawFrame(canvas, mFrame, x, y, width, height); + + if (topIndex == 0) { + drawIcon(canvas, width, height, dataSourceType); + } + + if (topIndex == 0) { + ResourceTexture icon = null; + if (isLocal(dataSourceType)) { + icon = mLocalAlbumIcon; + } else if (chooseToCache) { + icon = mCheckedItem; + } else { + icon = mUnCheckedItem; + } + + int w = ICON_SIZE; + int h = ICON_SIZE; + x = width / 2 - w / 2; + y = -height / 2 - h / 2; + + icon.draw(canvas, x, y, w, h); + + if (isCaching) { + int textWidth = mCaching.getWidth(); + int textHeight = mCaching.getHeight(); + x = -textWidth / 2; + y = height / 2 - textHeight; + + // Leave a few pixels of margin in the background rect. + float sideMargin = Utils.clamp(textWidth * 0.1f, 2.0f, + 6.0f); + float clearance = Utils.clamp(textHeight * 0.1f, 2.0f, + 6.0f); + + // Overlay the "Caching" wording at the bottom-center of the content. + canvas.fillRect(x - sideMargin, y - clearance, + textWidth + sideMargin * 2, textHeight + clearance, + COLOR_CACHING_BACKGROUND); + mCaching.draw(canvas, x, y); + } + } + } + + @Override + public void drawFocus(GLCanvas canvas, int width, int height) { + } +} diff --git a/src/com/android/gallery3d/ui/MeasureHelper.java b/src/com/android/gallery3d/ui/MeasureHelper.java new file mode 100644 index 000000000..f65dc10b3 --- /dev/null +++ b/src/com/android/gallery3d/ui/MeasureHelper.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Rect; +import android.view.View.MeasureSpec; + +class MeasureHelper { + + private static MeasureHelper sInstance = new MeasureHelper(null); + + private GLView mComponent; + private int mPreferredWidth; + private int mPreferredHeight; + + private MeasureHelper(GLView component) { + mComponent = component; + } + + public static MeasureHelper getInstance(GLView component) { + sInstance.mComponent = component; + return sInstance; + } + + public MeasureHelper setPreferredContentSize(int width, int height) { + mPreferredWidth = width; + mPreferredHeight = height; + return this; + } + + public void measure(int widthSpec, int heightSpec) { + Rect p = mComponent.getPaddings(); + setMeasuredSize( + getLength(widthSpec, mPreferredWidth + p.left + p.right), + getLength(heightSpec, mPreferredHeight + p.top + p.bottom)); + } + + private static int getLength(int measureSpec, int prefered) { + int specLength = MeasureSpec.getSize(measureSpec); + switch(MeasureSpec.getMode(measureSpec)) { + case MeasureSpec.EXACTLY: return specLength; + case MeasureSpec.AT_MOST: return Math.min(prefered, specLength); + default: return prefered; + } + } + + protected void setMeasuredSize(int width, int height) { + mComponent.setMeasuredSize(width, height); + } + +} diff --git a/src/com/android/gallery3d/ui/MenuExecutor.java b/src/com/android/gallery3d/ui/MenuExecutor.java new file mode 100644 index 000000000..710ddc422 --- /dev/null +++ b/src/com/android/gallery3d/ui/MenuExecutor.java @@ -0,0 +1,398 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.CropImage; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.Toast; + +import java.util.ArrayList; + +public class MenuExecutor { + @SuppressWarnings("unused") + private static final String TAG = "MenuExecutor"; + + private static final int MSG_TASK_COMPLETE = 1; + private static final int MSG_TASK_UPDATE = 2; + private static final int MSG_DO_SHARE = 3; + + public static final int EXECUTION_RESULT_SUCCESS = 1; + public static final int EXECUTION_RESULT_FAIL = 2; + public static final int EXECUTION_RESULT_CANCEL = 3; + + private ProgressDialog mDialog; + private Future mTask; + + private final GalleryActivity mActivity; + private final SelectionManager mSelectionManager; + private final Handler mHandler; + + private static ProgressDialog showProgressDialog( + Context context, int titleId, int progressMax) { + ProgressDialog dialog = new ProgressDialog(context); + dialog.setTitle(titleId); + dialog.setMax(progressMax); + dialog.setCancelable(false); + dialog.setIndeterminate(false); + if (progressMax > 1) { + dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + } + dialog.show(); + return dialog; + } + + public interface ProgressListener { + public void onProgressUpdate(int index); + public void onProgressComplete(int result); + } + + public MenuExecutor( + GalleryActivity activity, SelectionManager selectionManager) { + mActivity = Utils.checkNotNull(activity); + mSelectionManager = Utils.checkNotNull(selectionManager); + mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_TASK_COMPLETE: { + if (mDialog != null) { + mDialog.dismiss(); + mDialog = null; + mTask = null; + } + if (message.obj != null) { + ProgressListener listener = (ProgressListener) message.obj; + listener.onProgressComplete(message.arg1); + } + mSelectionManager.leaveSelectionMode(); + break; + } + case MSG_TASK_UPDATE: { + if (mDialog != null) mDialog.setProgress(message.arg1); + if (message.obj != null) { + ProgressListener listener = (ProgressListener) message.obj; + listener.onProgressUpdate(message.arg1); + } + break; + } + case MSG_DO_SHARE: { + ((Activity) mActivity).startActivity((Intent) message.obj); + break; + } + } + } + }; + } + + public void pause() { + if (mTask != null) { + mTask.cancel(); + mTask.waitDone(); + mDialog.dismiss(); + mDialog = null; + mTask = null; + } + } + + private void onProgressUpdate(int index, ProgressListener listener) { + mHandler.sendMessage( + mHandler.obtainMessage(MSG_TASK_UPDATE, index, 0, listener)); + } + + private void onProgressComplete(int result, ProgressListener listener) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_TASK_COMPLETE, result, 0, listener)); + } + + private int getShareType(SelectionManager selectionManager) { + ArrayList items = selectionManager.getSelected(false); + int type = 0; + DataManager dataManager = mActivity.getDataManager(); + for (Path id : items) { + type |= dataManager.getMediaType(id); + } + return type; + } + + private void onShareItemClicked(final SelectionManager selectionManager, + final String mimeType, final ComponentName component) { + Utils.assertTrue(mDialog == null); + final ArrayList items = selectionManager.getSelected(true); + mDialog = showProgressDialog((Activity) mActivity, + R.string.loading_image, items.size()); + + mTask = mActivity.getThreadPool().submit(new Job() { + @Override + public Void run(JobContext jc) { + DataManager manager = mActivity.getDataManager(); + ArrayList uris = new ArrayList(items.size()); + int index = 0; + for (Path path : items) { + if ((manager.getSupportedOperations(path) + & MediaObject.SUPPORT_SHARE) != 0) { + uris.add(manager.getContentUri(path)); + } + onProgressUpdate(++index, null); + } + if (jc.isCancelled()) return null; + Intent intent = new Intent() + .setComponent(component).setType(mimeType); + if (uris.isEmpty()) { + return null; + } else if (uris.size() == 1) { + intent.setAction(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); + } else { + intent.setAction(Intent.ACTION_SEND_MULTIPLE); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + } + onProgressComplete(EXECUTION_RESULT_SUCCESS, null); + mHandler.sendMessage(mHandler.obtainMessage(MSG_DO_SHARE, intent)); + return null; + } + }, null); + } + + private static void setMenuItemVisibility( + Menu menu, int id, boolean visibility) { + MenuItem item = menu.findItem(id); + if (item != null) item.setVisible(visibility); + } + + public static void updateMenuOperation(Menu menu, int supported) { + boolean supportDelete = (supported & MediaObject.SUPPORT_DELETE) != 0; + boolean supportRotate = (supported & MediaObject.SUPPORT_ROTATE) != 0; + boolean supportCrop = (supported & MediaObject.SUPPORT_CROP) != 0; + boolean supportShare = (supported & MediaObject.SUPPORT_SHARE) != 0; + boolean supportSetAs = (supported & MediaObject.SUPPORT_SETAS) != 0; + boolean supportShowOnMap = (supported & MediaObject.SUPPORT_SHOW_ON_MAP) != 0; + boolean supportCache = (supported & MediaObject.SUPPORT_CACHE) != 0; + boolean supportEdit = (supported & MediaObject.SUPPORT_EDIT) != 0; + boolean supportInfo = (supported & MediaObject.SUPPORT_INFO) != 0; + boolean supportImport = (supported & MediaObject.SUPPORT_IMPORT) != 0; + + setMenuItemVisibility(menu, R.id.action_delete, supportDelete); + setMenuItemVisibility(menu, R.id.action_rotate_ccw, supportRotate); + setMenuItemVisibility(menu, R.id.action_rotate_cw, supportRotate); + setMenuItemVisibility(menu, R.id.action_crop, supportCrop); + setMenuItemVisibility(menu, R.id.action_share, supportShare); + setMenuItemVisibility(menu, R.id.action_setas, supportSetAs); + setMenuItemVisibility(menu, R.id.action_show_on_map, supportShowOnMap); + setMenuItemVisibility(menu, R.id.action_edit, supportEdit); + setMenuItemVisibility(menu, R.id.action_details, supportInfo); + setMenuItemVisibility(menu, R.id.action_import, supportImport); + } + + private Path getSingleSelectedPath() { + ArrayList ids = mSelectionManager.getSelected(true); + Utils.assertTrue(ids.size() == 1); + return ids.get(0); + } + + public boolean onMenuClicked(MenuItem menuItem, ProgressListener listener) { + int title; + DataManager manager = mActivity.getDataManager(); + int action = menuItem.getItemId(); + switch (action) { + case R.id.action_select_all: + if (mSelectionManager.inSelectAllMode()) { + mSelectionManager.deSelectAll(); + } else { + mSelectionManager.selectAll(); + } + return true; + case R.id.action_crop: { + Path path = getSingleSelectedPath(); + String mimeType = getMimeType(manager.getMediaType(path)); + Intent intent = new Intent(CropImage.ACTION_CROP) + .setDataAndType(manager.getContentUri(path), mimeType); + ((Activity) mActivity).startActivity(intent); + return true; + } + case R.id.action_setas: { + Path path = getSingleSelectedPath(); + int type = manager.getMediaType(path); + Intent intent = new Intent(Intent.ACTION_ATTACH_DATA); + String mimeType = getMimeType(type); + intent.setDataAndType(manager.getContentUri(path), mimeType); + intent.putExtra("mimeType", mimeType); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + Activity activity = (Activity) mActivity; + activity.startActivity(Intent.createChooser( + intent, activity.getString(R.string.set_as))); + return true; + } + case R.id.action_confirm_delete: + title = R.string.delete; + break; + case R.id.action_rotate_cw: + title = R.string.rotate_right; + break; + case R.id.action_rotate_ccw: + title = R.string.rotate_left; + break; + case R.id.action_show_on_map: + title = R.string.show_on_map; + break; + case R.id.action_edit: + title = R.string.edit; + break; + case R.id.action_import: + title = R.string.Import; + break; + default: + return false; + } + startAction(action, title, listener); + return true; + } + + public void startAction(int action, int title, ProgressListener listener) { + ArrayList ids = mSelectionManager.getSelected(false); + Utils.assertTrue(mDialog == null); + + Activity activity = (Activity) mActivity; + mDialog = showProgressDialog(activity, title, ids.size()); + MediaOperation operation = new MediaOperation(action, ids, listener); + mTask = mActivity.getThreadPool().submit(operation, null); + } + + public static String getMimeType(int type) { + switch (type) { + case MediaObject.MEDIA_TYPE_IMAGE : + return "image/*"; + case MediaObject.MEDIA_TYPE_VIDEO : + return "video/*"; + default: return "*/*"; + } + } + + private boolean execute( + DataManager manager, JobContext jc, int cmd, Path path) { + boolean result = true; + switch (cmd) { + case R.id.action_confirm_delete: + manager.delete(path); + break; + case R.id.action_rotate_cw: + manager.rotate(path, 90); + break; + case R.id.action_rotate_ccw: + manager.rotate(path, -90); + break; + case R.id.action_toggle_full_caching: { + MediaObject obj = manager.getMediaObject(path); + int cacheFlag = obj.getCacheFlag(); + if (cacheFlag == MediaObject.CACHE_FLAG_FULL) { + cacheFlag = MediaObject.CACHE_FLAG_SCREENNAIL; + } else { + cacheFlag = MediaObject.CACHE_FLAG_FULL; + } + obj.cache(cacheFlag); + break; + } + case R.id.action_show_on_map: { + MediaItem item = (MediaItem) manager.getMediaObject(path); + double latlng[] = new double[2]; + item.getLatLong(latlng); + if (GalleryUtils.isValidLocation(latlng[0], latlng[1])) { + GalleryUtils.showOnMap((Context) mActivity, latlng[0], latlng[1]); + } + break; + } + case R.id.action_import: { + MediaObject obj = manager.getMediaObject(path); + result = obj.Import(); + break; + } + case R.id.action_edit: { + Activity activity = (Activity) mActivity; + MediaItem item = (MediaItem) manager.getMediaObject(path); + try { + activity.startActivity(Intent.createChooser( + new Intent(Intent.ACTION_EDIT) + .setData(item.getContentUri()) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION), + null)); + } catch (Throwable t) { + Log.w(TAG, "failed to start edit activity: ", t); + Toast.makeText(activity, + activity.getString(R.string.activity_not_found), + Toast.LENGTH_SHORT).show(); + } + break; + } + default: + throw new AssertionError(); + } + return result; + } + + private class MediaOperation implements Job { + private final ArrayList mItems; + private final int mOperation; + private final ProgressListener mListener; + + public MediaOperation(int operation, ArrayList items, ProgressListener listener) { + mOperation = operation; + mItems = items; + mListener = listener; + } + + public Void run(JobContext jc) { + int index = 0; + DataManager manager = mActivity.getDataManager(); + int result = EXECUTION_RESULT_SUCCESS; + for (Path id : mItems) { + if (jc.isCancelled()) { + result = EXECUTION_RESULT_CANCEL; + break; + } + try { + if (!execute(manager, jc, mOperation, id)) result = EXECUTION_RESULT_FAIL; + } catch (Throwable th) { + Log.e(TAG, "failed to execute operation " + mOperation + + " for " + id, th); + } + onProgressUpdate(index++, mListener); + } + onProgressComplete(result, mListener); + return null; + } + } +} + diff --git a/src/com/android/gallery3d/ui/MultiLineTexture.java b/src/com/android/gallery3d/ui/MultiLineTexture.java new file mode 100644 index 000000000..be62d59c0 --- /dev/null +++ b/src/com/android/gallery3d/ui/MultiLineTexture.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; + +// MultiLineTexture is a texture shows the content of a specified String. +// +// To create a MultiLineTexture, use the newInstance() method and specify +// the String, the font size, and the color. +class MultiLineTexture extends CanvasTexture { + private final Layout mLayout; + + private MultiLineTexture(Layout layout) { + super(layout.getWidth(), layout.getHeight()); + mLayout = layout; + } + + public static MultiLineTexture newInstance( + String text, int maxWidth, float textSize, int color) { + TextPaint paint = StringTexture.getDefaultPaint(textSize, color); + Layout layout = new StaticLayout(text, 0, text.length(), paint, + maxWidth, Layout.Alignment.ALIGN_NORMAL, 1, 0, true, null, 0); + + return new MultiLineTexture(layout); + } + + @Override + protected void onDraw(Canvas canvas, Bitmap backing) { + mLayout.draw(canvas); + } +} diff --git a/src/com/android/gallery3d/ui/NinePatchChunk.java b/src/com/android/gallery3d/ui/NinePatchChunk.java new file mode 100644 index 000000000..61bf22c33 --- /dev/null +++ b/src/com/android/gallery3d/ui/NinePatchChunk.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Rect; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +// See "frameworks/base/include/utils/ResourceTypes.h" for the format of +// NinePatch chunk. +class NinePatchChunk { + + public static final int NO_COLOR = 0x00000001; + public static final int TRANSPARENT_COLOR = 0x00000000; + + public Rect mPaddings = new Rect(); + + public int mDivX[]; + public int mDivY[]; + public int mColor[]; + + private static void readIntArray(int[] data, ByteBuffer buffer) { + for (int i = 0, n = data.length; i < n; ++i) { + data[i] = buffer.getInt(); + } + } + + private static void checkDivCount(int length) { + if (length == 0 || (length & 0x01) != 0) { + throw new RuntimeException("invalid nine-patch: " + length); + } + } + + public static NinePatchChunk deserialize(byte[] data) { + ByteBuffer byteBuffer = + ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()); + + byte wasSerialized = byteBuffer.get(); + if (wasSerialized == 0) return null; + + NinePatchChunk chunk = new NinePatchChunk(); + chunk.mDivX = new int[byteBuffer.get()]; + chunk.mDivY = new int[byteBuffer.get()]; + chunk.mColor = new int[byteBuffer.get()]; + + checkDivCount(chunk.mDivX.length); + checkDivCount(chunk.mDivY.length); + + // skip 8 bytes + byteBuffer.getInt(); + byteBuffer.getInt(); + + chunk.mPaddings.left = byteBuffer.getInt(); + chunk.mPaddings.right = byteBuffer.getInt(); + chunk.mPaddings.top = byteBuffer.getInt(); + chunk.mPaddings.bottom = byteBuffer.getInt(); + + // skip 4 bytes + byteBuffer.getInt(); + + readIntArray(chunk.mDivX, byteBuffer); + readIntArray(chunk.mDivY, byteBuffer); + readIntArray(chunk.mColor, byteBuffer); + + return chunk; + } +} \ No newline at end of file diff --git a/src/com/android/gallery3d/ui/NinePatchTexture.java b/src/com/android/gallery3d/ui/NinePatchTexture.java new file mode 100644 index 000000000..15b057a92 --- /dev/null +++ b/src/com/android/gallery3d/ui/NinePatchTexture.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.common.Utils; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Rect; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.microedition.khronos.opengles.GL11; + +// NinePatchTexture is a texture backed by a NinePatch resource. +// +// getPaddings() returns paddings specified in the NinePatch. +// getNinePatchChunk() returns the layout data specified in the NinePatch. +// +public class NinePatchTexture extends ResourceTexture { + @SuppressWarnings("unused") + private static final String TAG = "NinePatchTexture"; + private NinePatchChunk mChunk; + private MyCacheMap mInstanceCache = + new MyCacheMap(); + + public NinePatchTexture(Context context, int resId) { + super(context, resId); + } + + @Override + protected Bitmap onGetBitmap() { + if (mBitmap != null) return mBitmap; + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap bitmap = BitmapFactory.decodeResource( + mContext.getResources(), mResId, options); + mBitmap = bitmap; + setSize(bitmap.getWidth(), bitmap.getHeight()); + byte[] chunkData = bitmap.getNinePatchChunk(); + mChunk = chunkData == null + ? null + : NinePatchChunk.deserialize(bitmap.getNinePatchChunk()); + if (mChunk == null) { + throw new RuntimeException("invalid nine-patch image: " + mResId); + } + return bitmap; + } + + public Rect getPaddings() { + // get the paddings from nine patch + if (mChunk == null) onGetBitmap(); + return mChunk.mPaddings; + } + + public NinePatchChunk getNinePatchChunk() { + if (mChunk == null) onGetBitmap(); + return mChunk; + } + + private static class MyCacheMap extends LinkedHashMap { + private int CACHE_SIZE = 16; + private V mJustRemoved; + + public MyCacheMap() { + super(4, 0.75f, true); + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + if (size() > CACHE_SIZE) { + mJustRemoved = eldest.getValue(); + return true; + } + return false; + } + + public V getJustRemoved() { + V result = mJustRemoved; + mJustRemoved = null; + return result; + } + } + + private NinePatchInstance findInstance(GLCanvas canvas, int w, int h) { + long key = w; + key = (key << 32) | h; + NinePatchInstance instance = mInstanceCache.get(key); + + if (instance == null) { + instance = new NinePatchInstance(this, w, h); + mInstanceCache.put(key, instance); + NinePatchInstance removed = mInstanceCache.getJustRemoved(); + if (removed != null) { + removed.recycle(canvas); + } + } + + return instance; + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + if (!isLoaded(canvas)) { + mInstanceCache.clear(); + } + + if (w != 0 && h != 0) { + findInstance(canvas, w, h).draw(canvas, this, x, y); + } + } + + @Override + public void recycle() { + super.recycle(); + GLCanvas canvas = mCanvasRef == null ? null : mCanvasRef.get(); + if (canvas == null) return; + for (NinePatchInstance instance : mInstanceCache.values()) { + instance.recycle(canvas); + } + mInstanceCache.clear(); + } +} + +// This keeps data for a specialization of NinePatchTexture with the size +// (width, height). We pre-compute the coordinates for efficiency. +class NinePatchInstance { + + @SuppressWarnings("unused") + private static final String TAG = "NinePatchInstance"; + + // We need 16 vertices for a normal nine-patch image (the 4x4 vertices) + private static final int VERTEX_BUFFER_SIZE = 16 * 2; + + // We need 22 indices for a normal nine-patch image, plus 2 for each + // transparent region. Current there are at most 1 transparent region. + private static final int INDEX_BUFFER_SIZE = 22 + 2; + + private FloatBuffer mXyBuffer; + private FloatBuffer mUvBuffer; + private ByteBuffer mIndexBuffer; + + // Names for buffer names: xy, uv, index. + private int[] mBufferNames; + + private int mIdxCount; + + public NinePatchInstance(NinePatchTexture tex, int width, int height) { + NinePatchChunk chunk = tex.getNinePatchChunk(); + + if (width <= 0 || height <= 0) { + throw new RuntimeException("invalid dimension"); + } + + // The code should be easily extended to handle the general cases by + // allocating more space for buffers. But let's just handle the only + // use case. + if (chunk.mDivX.length != 2 || chunk.mDivY.length != 2) { + throw new RuntimeException("unsupported nine patch"); + } + + float divX[] = new float[4]; + float divY[] = new float[4]; + float divU[] = new float[4]; + float divV[] = new float[4]; + + int nx = stretch(divX, divU, chunk.mDivX, tex.getWidth(), width); + int ny = stretch(divY, divV, chunk.mDivY, tex.getHeight(), height); + + prepareVertexData(divX, divY, divU, divV, nx, ny, chunk.mColor); + } + + /** + * Stretches the texture according to the nine-patch rules. It will + * linearly distribute the strechy parts defined in the nine-patch chunk to + * the target area. + * + *
+     *                      source
+     *          /--------------^---------------\
+     *         u0    u1       u2  u3     u4   u5
+     * div ---> |fffff|ssssssss|fff|ssssss|ffff| ---> u
+     *          |    div0    div1 div2   div3  |
+     *          |     |       /   /      /    /
+     *          |     |      /   /     /    /
+     *          |     |     /   /    /    /
+     *          |fffff|ssss|fff|sss|ffff| ---> x
+     *         x0    x1   x2  x3  x4   x5
+     *          \----------v------------/
+     *                  target
+     *
+     * f: fixed segment
+     * s: stretchy segment
+     * 
+ * + * @param div the stretch parts defined in nine-patch chunk + * @param source the length of the texture + * @param target the length on the drawing plan + * @param u output, the positions of these dividers in the texture + * coordinate + * @param x output, the corresponding position of these dividers on the + * drawing plan + * @return the number of these dividers. + */ + private static int stretch( + float x[], float u[], int div[], int source, int target) { + int textureSize = Utils.nextPowerOf2(source); + float textureBound = (float) source / textureSize; + + float stretch = 0; + for (int i = 0, n = div.length; i < n; i += 2) { + stretch += div[i + 1] - div[i]; + } + + float remaining = target - source + stretch; + + float lastX = 0; + float lastU = 0; + + x[0] = 0; + u[0] = 0; + for (int i = 0, n = div.length; i < n; i += 2) { + // Make the stretchy segment a little smaller to prevent sampling + // on neighboring fixed segments. + // fixed segment + x[i + 1] = lastX + (div[i] - lastU) + 0.5f; + u[i + 1] = Math.min((div[i] + 0.5f) / textureSize, textureBound); + + // stretchy segment + float partU = div[i + 1] - div[i]; + float partX = remaining * partU / stretch; + remaining -= partX; + stretch -= partU; + + lastX = x[i + 1] + partX; + lastU = div[i + 1]; + x[i + 2] = lastX - 0.5f; + u[i + 2] = Math.min((lastU - 0.5f)/ textureSize, textureBound); + } + // the last fixed segment + x[div.length + 1] = target; + u[div.length + 1] = textureBound; + + // remove segments with length 0. + int last = 0; + for (int i = 1, n = div.length + 2; i < n; ++i) { + if ((x[i] - x[last]) < 1f) continue; + x[++last] = x[i]; + u[last] = u[i]; + } + return last + 1; + } + + private void prepareVertexData(float x[], float y[], float u[], float v[], + int nx, int ny, int[] color) { + /* + * Given a 3x3 nine-patch image, the vertex order is defined as the + * following graph: + * + * (0) (1) (2) (3) + * | /| /| /| + * | / | / | / | + * (4) (5) (6) (7) + * | \ | \ | \ | + * | \| \| \| + * (8) (9) (A) (B) + * | /| /| /| + * | / | / | / | + * (C) (D) (E) (F) + * + * And we draw the triangle strip in the following index order: + * + * index: 04152637B6A5948C9DAEBF + */ + int pntCount = 0; + float xy[] = new float[VERTEX_BUFFER_SIZE]; + float uv[] = new float[VERTEX_BUFFER_SIZE]; + for (int j = 0; j < ny; ++j) { + for (int i = 0; i < nx; ++i) { + int xIndex = (pntCount++) << 1; + int yIndex = xIndex + 1; + xy[xIndex] = x[i]; + xy[yIndex] = y[j]; + uv[xIndex] = u[i]; + uv[yIndex] = v[j]; + } + } + + int idxCount = 1; + boolean isForward = false; + byte index[] = new byte[INDEX_BUFFER_SIZE]; + for (int row = 0; row < ny - 1; row++) { + --idxCount; + isForward = !isForward; + + int start, end, inc; + if (isForward) { + start = 0; + end = nx; + inc = 1; + } else { + start = nx - 1; + end = -1; + inc = -1; + } + + for (int col = start; col != end; col += inc) { + int k = row * nx + col; + if (col != start) { + int colorIdx = row * (nx - 1) + col; + if (isForward) colorIdx--; + if (color[colorIdx] == NinePatchChunk.TRANSPARENT_COLOR) { + index[idxCount] = index[idxCount - 1]; + ++idxCount; + index[idxCount++] = (byte) k; + } + } + + index[idxCount++] = (byte) k; + index[idxCount++] = (byte) (k + nx); + } + } + + mIdxCount = idxCount; + + int size = (pntCount * 2) * (Float.SIZE / Byte.SIZE); + mXyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); + mUvBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); + mIndexBuffer = allocateDirectNativeOrderBuffer(mIdxCount); + + mXyBuffer.put(xy, 0, pntCount * 2).position(0); + mUvBuffer.put(uv, 0, pntCount * 2).position(0); + mIndexBuffer.put(index, 0, idxCount).position(0); + } + + private static ByteBuffer allocateDirectNativeOrderBuffer(int size) { + return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); + } + + private void prepareBuffers(GLCanvas canvas) { + mBufferNames = new int[3]; + GL11 gl = canvas.getGLInstance(); + gl.glGenBuffers(3, mBufferNames, 0); + + gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBufferNames[0]); + gl.glBufferData(GL11.GL_ARRAY_BUFFER, + mXyBuffer.capacity() * (Float.SIZE / Byte.SIZE), + mXyBuffer, GL11.GL_STATIC_DRAW); + + gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBufferNames[1]); + gl.glBufferData(GL11.GL_ARRAY_BUFFER, + mUvBuffer.capacity() * (Float.SIZE / Byte.SIZE), + mUvBuffer, GL11.GL_STATIC_DRAW); + + gl.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, mBufferNames[2]); + gl.glBufferData(GL11.GL_ELEMENT_ARRAY_BUFFER, + mIndexBuffer.capacity(), + mIndexBuffer, GL11.GL_STATIC_DRAW); + + // These buffers are never used again. + mXyBuffer = null; + mUvBuffer = null; + mIndexBuffer = null; + } + + public void draw(GLCanvas canvas, NinePatchTexture tex, int x, int y) { + if (mBufferNames == null) { + prepareBuffers(canvas); + } + canvas.drawMesh(tex, x, y, mBufferNames[0], mBufferNames[1], + mBufferNames[2], mIdxCount); + } + + public void recycle(GLCanvas canvas) { + if (mBufferNames != null) { + canvas.deleteBuffer(mBufferNames[0]); + canvas.deleteBuffer(mBufferNames[1]); + canvas.deleteBuffer(mBufferNames[2]); + mBufferNames = null; + } + } +} diff --git a/src/com/android/gallery3d/ui/OnSelectedListener.java b/src/com/android/gallery3d/ui/OnSelectedListener.java new file mode 100644 index 000000000..2cc5809bf --- /dev/null +++ b/src/com/android/gallery3d/ui/OnSelectedListener.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +public interface OnSelectedListener { + public void onSelected(GLView source); +} diff --git a/src/com/android/gallery3d/ui/Paper.java b/src/com/android/gallery3d/ui/Paper.java new file mode 100644 index 000000000..641fc2c8e --- /dev/null +++ b/src/com/android/gallery3d/ui/Paper.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.ui.PositionRepository.Position; +import com.android.gallery3d.util.GalleryUtils; + +import android.opengl.Matrix; + +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11ExtensionPack; + +// This class does the overscroll effect. +class Paper { + private static final String TAG = "Paper"; + private static final int ROTATE_FACTOR = 4; + private OverscrollAnimation mAnimationLeft = new OverscrollAnimation(); + private OverscrollAnimation mAnimationRight = new OverscrollAnimation(); + private int mWidth, mHeight; + private float[] mMatrix = new float[16]; + + public void overScroll(float distance) { + if (distance < 0) { + mAnimationLeft.scroll(-distance); + } else { + mAnimationRight.scroll(distance); + } + } + + public boolean advanceAnimation(long currentTimeMillis) { + return mAnimationLeft.advanceAnimation(currentTimeMillis) + | mAnimationRight.advanceAnimation(currentTimeMillis); + } + + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + } + + public float[] getTransform(Position target, Position base, + float scrollX, float scrollY) { + float left = mAnimationLeft.getValue(); + float right = mAnimationRight.getValue(); + float screenX = target.x - scrollX; + float t = ((mWidth - screenX) * left - screenX * right) / (mWidth * mWidth); + // compress t to the range (-1, 1) by the function + // f(t) = (1 / (1 + e^-t) - 0.5) * 2 + // then multiply by 90 to make the range (-45, 45) + float degrees = + (1 / (1 + (float) Math.exp(-t * ROTATE_FACTOR)) - 0.5f) * 2 * -45; + Matrix.setIdentityM(mMatrix, 0); + Matrix.translateM(mMatrix, 0, mMatrix, 0, base.x, base.y, base.z); + Matrix.rotateM(mMatrix, 0, degrees, 0, 1, 0); + Matrix.translateM(mMatrix, 0, mMatrix, 0, + target.x - base.x, target.y - base.y, target.z - base.z); + return mMatrix; + } +} + +class OverscrollAnimation { + private static final String TAG = "OverscrollAnimation"; + private static final long START_ANIMATION = -1; + private static final long NO_ANIMATION = -2; + private static final long ANIMATION_DURATION = 500; + + private long mAnimationStartTime = NO_ANIMATION; + private float mVelocity; + private float mCurrentValue; + + public void scroll(float distance) { + mAnimationStartTime = START_ANIMATION; + mCurrentValue += distance; + } + + public boolean advanceAnimation(long currentTimeMillis) { + if (mAnimationStartTime == NO_ANIMATION) return false; + if (mAnimationStartTime == START_ANIMATION) { + mAnimationStartTime = currentTimeMillis; + return true; + } + + long deltaTime = currentTimeMillis - mAnimationStartTime; + float t = deltaTime / 100f; + mCurrentValue *= Math.pow(0.5f, t); + mAnimationStartTime = currentTimeMillis; + + if (mCurrentValue < 1) { + mAnimationStartTime = NO_ANIMATION; + mCurrentValue = 0; + return false; + } + return true; + } + + public float getValue() { + return mCurrentValue; + } +} diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java new file mode 100644 index 000000000..aba572b00 --- /dev/null +++ b/src/com/android/gallery3d/ui/PhotoView.java @@ -0,0 +1,1191 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.PositionRepository.Position; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.RectF; +import android.os.Message; +import android.os.SystemClock; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; + +public class PhotoView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "PhotoView"; + + public static final int INVALID_SIZE = -1; + + private static final int MSG_TRANSITION_COMPLETE = 1; + private static final int MSG_SHOW_LOADING = 2; + + private static final long DELAY_SHOW_LOADING = 250; // 250ms; + + private static final int TRANS_NONE = 0; + private static final int TRANS_SWITCH_NEXT = 3; + private static final int TRANS_SWITCH_PREVIOUS = 4; + + public static final int TRANS_SLIDE_IN_RIGHT = 1; + public static final int TRANS_SLIDE_IN_LEFT = 2; + public static final int TRANS_OPEN_ANIMATION = 5; + + private static final int LOADING_INIT = 0; + private static final int LOADING_TIMEOUT = 1; + private static final int LOADING_COMPLETE = 2; + private static final int LOADING_FAIL = 3; + + private static final int ENTRY_PREVIOUS = 0; + private static final int ENTRY_NEXT = 1; + + private static final int IMAGE_GAP = 96; + private static final int SWITCH_THRESHOLD = 256; + private static final float SWIPE_THRESHOLD = 300f; + + private static final float DEFAULT_TEXT_SIZE = 20; + + // We try to scale up the image to fill the screen. But in order not to + // scale too much for small icons, we limit the max up-scaling factor here. + private static final float SCALE_LIMIT = 4; + + public interface PhotoTapListener { + public void onSingleTapUp(int x, int y); + } + + // the previous/next image entries + private final ScreenNailEntry mScreenNails[] = new ScreenNailEntry[2]; + + private final ScaleGestureDetector mScaleDetector; + private final GestureDetector mGestureDetector; + private final DownUpDetector mDownUpDetector; + + private PhotoTapListener mPhotoTapListener; + + private final PositionController mPositionController; + + private Model mModel; + private StringTexture mLoadingText; + private StringTexture mNoThumbnailText; + private int mTransitionMode = TRANS_NONE; + private final TileImageView mTileView; + private Texture mVideoPlayIcon; + + private boolean mShowVideoPlayIcon; + private ProgressSpinner mLoadingSpinner; + + private SynchronizedHandler mHandler; + + private int mLoadingState = LOADING_COMPLETE; + + private RectF mTempRect = new RectF(); + private float[] mTempPoints = new float[8]; + + private int mImageRotation; + + private Path mOpenedItemPath; + private GalleryActivity mActivity; + + public PhotoView(GalleryActivity activity) { + mActivity = activity; + mTileView = new TileImageView(activity); + addComponent(mTileView); + Context context = activity.getAndroidContext(); + mLoadingSpinner = new ProgressSpinner(context); + mLoadingText = StringTexture.newInstance( + context.getString(R.string.loading), + DEFAULT_TEXT_SIZE, Color.WHITE); + mNoThumbnailText = StringTexture.newInstance( + context.getString(R.string.no_thumbnail), + DEFAULT_TEXT_SIZE, Color.WHITE); + + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_TRANSITION_COMPLETE: { + onTransitionComplete(); + break; + } + case MSG_SHOW_LOADING: { + if (mLoadingState == LOADING_INIT) { + // We don't need the opening animation + mOpenedItemPath = null; + + mLoadingSpinner.startAnimation(); + mLoadingState = LOADING_TIMEOUT; + invalidate(); + } + break; + } + default: throw new AssertionError(message.what); + } + } + }; + + mGestureDetector = new GestureDetector(context, + new MyGestureListener(), null, true /* ignoreMultitouch */); + mScaleDetector = new ScaleGestureDetector(context, new MyScaleListener()); + mDownUpDetector = new DownUpDetector(new MyDownUpListener()); + + for (int i = 0, n = mScreenNails.length; i < n; ++i) { + mScreenNails[i] = new ScreenNailEntry(); + } + + mPositionController = new PositionController(this); + mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play); + } + + + public void setModel(Model model) { + if (mModel == model) return; + mModel = model; + mTileView.setModel(model); + if (model != null) notifyOnNewImage(); + } + + public void setPhotoTapListener(PhotoTapListener listener) { + mPhotoTapListener = listener; + } + + private boolean setTileViewPosition(int centerX, int centerY, float scale) { + int inverseX = mPositionController.mImageW - centerX; + int inverseY = mPositionController.mImageH - centerY; + TileImageView t = mTileView; + int rotation = mImageRotation; + switch (rotation) { + case 0: return t.setPosition(centerX, centerY, scale, 0); + case 90: return t.setPosition(centerY, inverseX, scale, 90); + case 180: return t.setPosition(inverseX, inverseY, scale, 180); + case 270: return t.setPosition(inverseY, centerX, scale, 270); + default: throw new IllegalArgumentException(String.valueOf(rotation)); + } + } + + public void setPosition(int centerX, int centerY, float scale) { + if (setTileViewPosition(centerX, centerY, scale)) { + layoutScreenNails(); + } + } + + private void updateScreenNailEntry(int which, ImageData data) { + if (mTransitionMode == TRANS_SWITCH_NEXT + || mTransitionMode == TRANS_SWITCH_PREVIOUS) { + // ignore screen nail updating during switching + return; + } + ScreenNailEntry entry = mScreenNails[which]; + if (data == null) { + entry.set(false, null, 0); + } else { + entry.set(true, data.bitmap, data.rotation); + } + } + + // -1 previous, 0 current, 1 next + public void notifyImageInvalidated(int which) { + switch (which) { + case -1: { + updateScreenNailEntry( + ENTRY_PREVIOUS, mModel.getPreviousImage()); + layoutScreenNails(); + invalidate(); + break; + } + case 1: { + updateScreenNailEntry(ENTRY_NEXT, mModel.getNextImage()); + layoutScreenNails(); + invalidate(); + break; + } + case 0: { + // mImageWidth and mImageHeight will get updated + mTileView.notifyModelInvalidated(); + + mImageRotation = mModel.getImageRotation(); + if (((mImageRotation / 90) & 1) == 0) { + mPositionController.setImageSize( + mTileView.mImageWidth, mTileView.mImageHeight); + } else { + mPositionController.setImageSize( + mTileView.mImageHeight, mTileView.mImageWidth); + } + updateLoadingState(); + break; + } + } + } + + private void updateLoadingState() { + // Possible transitions of mLoadingState: + // INIT --> TIMEOUT, COMPLETE, FAIL + // TIMEOUT --> COMPLETE, FAIL, INIT + // COMPLETE --> INIT + // FAIL --> INIT + if (mModel.getLevelCount() != 0 || mModel.getBackupImage() != null) { + mHandler.removeMessages(MSG_SHOW_LOADING); + mLoadingState = LOADING_COMPLETE; + } else if (mModel.isFailedToLoad()) { + mHandler.removeMessages(MSG_SHOW_LOADING); + mLoadingState = LOADING_FAIL; + } else if (mLoadingState != LOADING_INIT) { + mLoadingState = LOADING_INIT; + mHandler.removeMessages(MSG_SHOW_LOADING); + mHandler.sendEmptyMessageDelayed( + MSG_SHOW_LOADING, DELAY_SHOW_LOADING); + } + } + + public void notifyModelInvalidated() { + if (mModel == null) { + updateScreenNailEntry(ENTRY_PREVIOUS, null); + updateScreenNailEntry(ENTRY_NEXT, null); + } else { + updateScreenNailEntry(ENTRY_PREVIOUS, mModel.getPreviousImage()); + updateScreenNailEntry(ENTRY_NEXT, mModel.getNextImage()); + } + layoutScreenNails(); + + if (mModel == null) { + mTileView.notifyModelInvalidated(); + mImageRotation = 0; + mPositionController.setImageSize(0, 0); + updateLoadingState(); + } else { + notifyImageInvalidated(0); + } + } + + @Override + protected boolean onTouch(MotionEvent event) { + mGestureDetector.onTouchEvent(event); + mScaleDetector.onTouchEvent(event); + mDownUpDetector.onTouchEvent(event); + return true; + } + + @Override + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + mTileView.layout(left, top, right, bottom); + if (changeSize) { + mPositionController.setViewSize(getWidth(), getHeight()); + for (ScreenNailEntry entry : mScreenNails) { + entry.updateDrawingSize(); + } + } + } + + private static int gapToSide(int imageWidth, int viewWidth) { + return Math.max(0, (viewWidth - imageWidth) / 2); + } + + private RectF getImageBounds() { + PositionController p = mPositionController; + float points[] = mTempPoints; + + /* + * (p0,p1)----------(p2,p3) + * | | + * | | + * (p4,p5)----------(p6,p7) + */ + points[0] = points[4] = -p.mCurrentX; + points[1] = points[3] = -p.mCurrentY; + points[2] = points[6] = p.mImageW - p.mCurrentX; + points[5] = points[7] = p.mImageH - p.mCurrentY; + + RectF rect = mTempRect; + rect.set(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, + Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); + + float scale = p.mCurrentScale; + float offsetX = p.mViewW / 2; + float offsetY = p.mViewH / 2; + for (int i = 0; i < 4; ++i) { + float x = points[i + i] * scale + offsetX; + float y = points[i + i + 1] * scale + offsetY; + if (x < rect.left) rect.left = x; + if (x > rect.right) rect.right = x; + if (y < rect.top) rect.top = y; + if (y > rect.bottom) rect.bottom = y; + } + return rect; + } + + + /* + * Here is how we layout the screen nails + * + * previous current next + * ___________ ________________ __________ + * | _______ | | __________ | | ______ | + * | | | | | | right->| | | | | | + * | | |<-------->|<--left | | | | | | + * | |_______| | | | |__________| | | |______| | + * |___________| | |________________| |__________| + * | <--> gapToSide() + * | + * IMAGE_GAP + Max(previous.gapToSide(), current.gapToSide) + */ + private void layoutScreenNails() { + int width = getWidth(); + int height = getHeight(); + + // Use the image width in AC, since we may fake the size if the + // image is unavailable + RectF bounds = getImageBounds(); + int left = Math.round(bounds.left); + int right = Math.round(bounds.right); + int gap = gapToSide(right - left, width); + + // layout the previous image + ScreenNailEntry entry = mScreenNails[ENTRY_PREVIOUS]; + + if (entry.isEnabled()) { + entry.layoutRightEdgeAt(left - ( + IMAGE_GAP + Math.max(gap, entry.gapToSide()))); + } + + // layout the next image + entry = mScreenNails[ENTRY_NEXT]; + if (entry.isEnabled()) { + entry.layoutLeftEdgeAt(right + ( + IMAGE_GAP + Math.max(gap, entry.gapToSide()))); + } + } + + private static class PositionController { + private long mAnimationStartTime = NO_ANIMATION; + private static final long NO_ANIMATION = -1; + private static final long LAST_ANIMATION = -2; + + // Animation time in milliseconds. + private static final float ANIM_TIME_SCROLL = 0; + private static final float ANIM_TIME_SCALE = 50; + private static final float ANIM_TIME_SNAPBACK = 600; + private static final float ANIM_TIME_SLIDE = 400; + private static final float ANIM_TIME_ZOOM = 300; + + private int mAnimationKind; + private final static int ANIM_KIND_SCROLL = 0; + private final static int ANIM_KIND_SCALE = 1; + private final static int ANIM_KIND_SNAPBACK = 2; + private final static int ANIM_KIND_SLIDE = 3; + private final static int ANIM_KIND_ZOOM = 4; + + private PhotoView mViewer; + private int mImageW, mImageH; + private int mViewW, mViewH; + + // The X, Y are the coordinate on bitmap which shows on the center of + // the view. We always keep the mCurrent{X,Y,SCALE} sync with the actual + // values used currently. + private int mCurrentX, mFromX, mToX; + private int mCurrentY, mFromY, mToY; + private float mCurrentScale, mFromScale, mToScale; + + // The offsets from the center of the view to the user's focus point, + // converted to the bitmap domain. + private float mPrevOffsetX; + private float mPrevOffsetY; + private boolean mInScale; + private boolean mUseViewSize = true; + + // The limits for position and scale. + private float mScaleMin, mScaleMax = 4f; + + PositionController(PhotoView viewer) { + mViewer = viewer; + } + + public void setImageSize(int width, int height) { + + // If no image available, use view size. + if (width == 0 || height == 0) { + mUseViewSize = true; + mImageW = mViewW; + mImageH = mViewH; + mCurrentX = mImageW / 2; + mCurrentY = mImageH / 2; + mCurrentScale = 1; + mScaleMin = 1; + mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); + return; + } + + mUseViewSize = false; + + float ratio = Math.min( + (float) mImageW / width, (float) mImageH / height); + + mCurrentX = translate(mCurrentX, mImageW, width, ratio); + mCurrentY = translate(mCurrentY, mImageH, height, ratio); + mCurrentScale = mCurrentScale * ratio; + + mFromX = translate(mFromX, mImageW, width, ratio); + mFromY = translate(mFromY, mImageH, height, ratio); + mFromScale = mFromScale * ratio; + + mToX = translate(mToX, mImageW, width, ratio); + mToY = translate(mToY, mImageH, height, ratio); + mToScale = mToScale * ratio; + + mImageW = width; + mImageH = height; + + mScaleMin = getMinimalScale(width, height, 0); + + // Scale the new image to fit into the old one + if (mViewer.mOpenedItemPath != null) { + Position position = PositionRepository + .getInstance(mViewer.mActivity).get(Long.valueOf( + System.identityHashCode(mViewer.mOpenedItemPath))); + mViewer.mOpenedItemPath = null; + if (position != null) { + float scale = 240f / Math.min(width, height); + mCurrentX = Math.round((mViewW / 2f - position.x) / scale) + mImageW / 2; + mCurrentY = Math.round((mViewH / 2f - position.y) / scale) + mImageH / 2; + mCurrentScale = scale; + mViewer.mTransitionMode = TRANS_OPEN_ANIMATION; + startSnapback(); + } + } else if (mAnimationStartTime == NO_ANIMATION) { + mCurrentScale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax); + } + mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); + } + + public void zoomIn(float tapX, float tapY, float targetScale) { + if (targetScale > mScaleMax) targetScale = mScaleMax; + float scale = mCurrentScale; + float tempX = (tapX - mViewW / 2) / mCurrentScale + mCurrentX; + float tempY = (tapY - mViewH / 2) / mCurrentScale + mCurrentY; + + // mCurrentX + (mViewW / 2) * (1 / targetScale) < mImageW + // mCurrentX - (mViewW / 2) * (1 / targetScale) > 0 + float min = mViewW / 2.0f / targetScale; + float max = mImageW - mViewW / 2.0f / targetScale; + int targetX = (int) Utils.clamp(tempX, min, max); + + min = mViewH / 2.0f / targetScale; + max = mImageH - mViewH / 2.0f / targetScale; + int targetY = (int) Utils.clamp(tempY, min, max); + + // If the width of the image is less then the view, center the image + if (mImageW * targetScale < mViewW) targetX = mImageW / 2; + if (mImageH * targetScale < mViewH) targetY = mImageH / 2; + + startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM); + } + + public void resetToFullView() { + startAnimation(mImageW / 2, mImageH / 2, mScaleMin, ANIM_KIND_ZOOM); + } + + private float getMinimalScale(int w, int h, int rotation) { + return Math.min(SCALE_LIMIT, ((rotation / 90) & 0x01) == 0 + ? Math.min((float) mViewW / w, (float) mViewH / h) + : Math.min((float) mViewW / h, (float) mViewH / w)); + } + + private static int translate(int value, int size, int updateSize, float ratio) { + return Math.round( + (value + (updateSize * ratio - size) / 2f) / ratio); + } + + public void setViewSize(int viewW, int viewH) { + boolean needLayout = mViewW == 0 || mViewH == 0; + + mViewW = viewW; + mViewH = viewH; + + if (mUseViewSize) { + mImageW = viewW; + mImageH = viewH; + mCurrentX = mImageW / 2; + mCurrentY = mImageH / 2; + mCurrentScale = 1; + mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); + } else { + boolean wasMinScale = (mCurrentScale == mScaleMin); + mScaleMin = Math.min(SCALE_LIMIT, Math.min( + (float) viewW / mImageW, (float) viewH / mImageH)); + if (needLayout || mCurrentScale < mScaleMin || wasMinScale) { + mCurrentX = mImageW / 2; + mCurrentY = mImageH / 2; + mCurrentScale = mScaleMin; + mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); + } + } + } + + public void stopAnimation() { + mAnimationStartTime = NO_ANIMATION; + } + + public void skipAnimation() { + if (mAnimationStartTime == NO_ANIMATION) return; + mAnimationStartTime = NO_ANIMATION; + mCurrentX = mToX; + mCurrentY = mToY; + mCurrentScale = mToScale; + } + + public void scrollBy(float dx, float dy, int type) { + startAnimation(getTargetX() + Math.round(dx / mCurrentScale), + getTargetY() + Math.round(dy / mCurrentScale), + mCurrentScale, type); + } + + public void beginScale(float focusX, float focusY) { + mInScale = true; + mPrevOffsetX = (focusX - mViewW / 2f) / mCurrentScale; + mPrevOffsetY = (focusY - mViewH / 2f) / mCurrentScale; + } + + public void scaleBy(float s, float focusX, float focusY) { + + // The focus point should keep this position on the ImageView. + // So, mCurrentX + mPrevOffsetX = mCurrentX' + offsetX. + // mCurrentY + mPrevOffsetY = mCurrentY' + offsetY. + float offsetX = (focusX - mViewW / 2f) / mCurrentScale; + float offsetY = (focusY - mViewH / 2f) / mCurrentScale; + + startAnimation(getTargetX() - Math.round(offsetX - mPrevOffsetX), + getTargetY() - Math.round(offsetY - mPrevOffsetY), + getTargetScale() * s, ANIM_KIND_SCALE); + mPrevOffsetX = offsetX; + mPrevOffsetY = offsetY; + } + + public void endScale() { + mInScale = false; + startSnapbackIfNeeded(); + } + + public void up() { + startSnapback(); + } + + public void startSlideInAnimation(int fromX) { + mFromX = Math.round(fromX + (mImageW - mViewW) / 2f); + mFromY = Math.round(mImageH / 2f); + mCurrentX = mFromX; + mCurrentY = mFromY; + startAnimation(mImageW / 2, mImageH / 2, mCurrentScale, + ANIM_KIND_SLIDE); + } + + public void startHorizontalSlide(int distance) { + scrollBy(distance, 0, ANIM_KIND_SLIDE); + } + + private void startAnimation( + int centerX, int centerY, float scale, int kind) { + if (centerX == mCurrentX && centerY == mCurrentY + && scale == mCurrentScale) return; + + mFromX = mCurrentX; + mFromY = mCurrentY; + mFromScale = mCurrentScale; + + mToX = centerX; + mToY = centerY; + mToScale = Utils.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax); + + // If the scaled dimension is smaller than the view, + // force it to be in the center. + if (Math.floor(mImageH * mToScale) <= mViewH) { + mToY = mImageH / 2; + } + + mAnimationStartTime = SystemClock.uptimeMillis(); + mAnimationKind = kind; + if (advanceAnimation()) mViewer.invalidate(); + } + + // Returns true if redraw is needed. + public boolean advanceAnimation() { + if (mAnimationStartTime == NO_ANIMATION) { + return false; + } else if (mAnimationStartTime == LAST_ANIMATION) { + mAnimationStartTime = NO_ANIMATION; + if (mViewer.mTransitionMode != TRANS_NONE) { + mViewer.mHandler.sendEmptyMessage(MSG_TRANSITION_COMPLETE); + return false; + } else { + return startSnapbackIfNeeded(); + } + } + + float animationTime; + if (mAnimationKind == ANIM_KIND_SCROLL) { + animationTime = ANIM_TIME_SCROLL; + } else if (mAnimationKind == ANIM_KIND_SCALE) { + animationTime = ANIM_TIME_SCALE; + } else if (mAnimationKind == ANIM_KIND_SLIDE) { + animationTime = ANIM_TIME_SLIDE; + } else if (mAnimationKind == ANIM_KIND_ZOOM) { + animationTime = ANIM_TIME_ZOOM; + } else /* if (mAnimationKind == ANIM_KIND_SNAPBACK) */ { + animationTime = ANIM_TIME_SNAPBACK; + } + + float progress; + if (animationTime == 0) { + progress = 1; + } else { + long now = SystemClock.uptimeMillis(); + progress = (now - mAnimationStartTime) / animationTime; + } + + if (progress >= 1) { + progress = 1; + mCurrentX = mToX; + mCurrentY = mToY; + mCurrentScale = mToScale; + mAnimationStartTime = LAST_ANIMATION; + } else { + float f = 1 - progress; + if (mAnimationKind == ANIM_KIND_SCROLL) { + progress = 1 - f; // linear + } else if (mAnimationKind == ANIM_KIND_SCALE) { + progress = 1 - f * f; // quadratic + } else /* if mAnimationKind is ANIM_KIND_SNAPBACK, + ANIM_KIND_ZOOM or ANIM_KIND_SLIDE */ { + progress = 1 - f * f * f * f * f; // x^5 + } + linearInterpolate(progress); + } + mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); + return true; + } + + private void linearInterpolate(float progress) { + // To linearly interpolate the position, we have to translate the + // coordinates. The meaning of the translated point (x, y) is the + // coordinates of the center of the bitmap on the view component. + float fromX = mViewW / 2f + (mImageW / 2f - mFromX) * mFromScale; + float toX = mViewW / 2f + (mImageW / 2f - mToX) * mToScale; + float currentX = fromX + progress * (toX - fromX); + + float fromY = mViewH / 2f + (mImageH / 2f - mFromY) * mFromScale; + float toY = mViewH / 2f + (mImageH / 2f - mToY) * mToScale; + float currentY = fromY + progress * (toY - fromY); + + mCurrentScale = mFromScale + progress * (mToScale - mFromScale); + mCurrentX = Math.round( + mImageW / 2f + (mViewW / 2f - currentX) / mCurrentScale); + mCurrentY = Math.round( + mImageH / 2f + (mViewH / 2f - currentY) / mCurrentScale); + } + + // Returns true if redraw is needed. + private boolean startSnapbackIfNeeded() { + if (mAnimationStartTime != NO_ANIMATION) return false; + if (mInScale) return false; + if (mAnimationKind == ANIM_KIND_SCROLL && mViewer.isDown()) { + return false; + } + return startSnapback(); + } + + public boolean startSnapback() { + boolean needAnimation = false; + int x = mCurrentX; + int y = mCurrentY; + float scale = mCurrentScale; + + if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) { + needAnimation = true; + scale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax); + } + + // The number of pixels when the edge is aligned. + int left = (int) Math.ceil(mViewW / (2 * scale)); + int right = mImageW - left; + int top = (int) Math.ceil(mViewH / (2 * scale)); + int bottom = mImageH - top; + + if (mImageW * scale > mViewW) { + if (mCurrentX < left) { + needAnimation = true; + x = left; + } else if (mCurrentX > right) { + needAnimation = true; + x = right; + } + } else if (mCurrentX != mImageW / 2) { + needAnimation = true; + x = mImageW / 2; + } + + if (mImageH * scale > mViewH) { + if (mCurrentY < top) { + needAnimation = true; + y = top; + } else if (mCurrentY > bottom) { + needAnimation = true; + y = bottom; + } + } else if (mCurrentY != mImageH / 2) { + needAnimation = true; + y = mImageH / 2; + } + + if (needAnimation) { + startAnimation(x, y, scale, ANIM_KIND_SNAPBACK); + } + + return needAnimation; + } + + private float getTargetScale() { + if (mAnimationStartTime == NO_ANIMATION + || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentScale; + return mToScale; + } + + private int getTargetX() { + if (mAnimationStartTime == NO_ANIMATION + || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentX; + return mToX; + } + + private int getTargetY() { + if (mAnimationStartTime == NO_ANIMATION + || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentY; + return mToY; + } + } + + @Override + protected void render(GLCanvas canvas) { + PositionController p = mPositionController; + + // Draw the current photo + if (mLoadingState == LOADING_COMPLETE) { + super.render(canvas); + } + + // Draw the previous and the next photo + if (mTransitionMode != TRANS_SLIDE_IN_LEFT + && mTransitionMode != TRANS_SLIDE_IN_RIGHT + && mTransitionMode != TRANS_OPEN_ANIMATION) { + ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS]; + ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT]; + + if (prevNail.mVisible) prevNail.draw(canvas); + if (nextNail.mVisible) nextNail.draw(canvas); + } + + // Draw the progress spinner and the text below it + // + // (x, y) is where we put the center of the spinner. + // s is the size of the video play icon, and we use s to layout text + // because we want to keep the text at the same place when the video + // play icon is shown instead of the spinner. + int w = getWidth(); + int h = getHeight(); + int x = Math.round(getImageBounds().centerX()); + int y = h / 2; + int s = Math.min(getWidth(), getHeight()) / 6; + + if (mLoadingState == LOADING_TIMEOUT) { + StringTexture m = mLoadingText; + ProgressSpinner r = mLoadingSpinner; + r.draw(canvas, x - r.getWidth() / 2, y - r.getHeight() / 2); + m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5); + invalidate(); // we need to keep the spinner rotating + } else if (mLoadingState == LOADING_FAIL) { + StringTexture m = mNoThumbnailText; + m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5); + } + + // Draw the video play icon (in the place where the spinner was) + if (mShowVideoPlayIcon + && mLoadingState != LOADING_INIT + && mLoadingState != LOADING_TIMEOUT) { + mVideoPlayIcon.draw(canvas, x - s / 2, y - s / 2, s, s); + } + + if (mPositionController.advanceAnimation()) invalidate(); + } + + private void stopCurrentSwipingIfNeeded() { + // Enable fast sweeping + if (mTransitionMode == TRANS_SWITCH_NEXT) { + mTransitionMode = TRANS_NONE; + mPositionController.stopAnimation(); + switchToNextImage(); + } else if (mTransitionMode == TRANS_SWITCH_PREVIOUS) { + mTransitionMode = TRANS_NONE; + mPositionController.stopAnimation(); + switchToPreviousImage(); + } + } + + private static boolean isAlmostEquals(float a, float b) { + float diff = a - b; + return (diff < 0 ? -diff : diff) < 0.02f; + } + + private boolean swipeImages(float velocity) { + if (mTransitionMode != TRANS_NONE + && mTransitionMode != TRANS_SWITCH_NEXT + && mTransitionMode != TRANS_SWITCH_PREVIOUS) return false; + + ScreenNailEntry next = mScreenNails[ENTRY_NEXT]; + ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS]; + + int width = getWidth(); + + // If the edge of the current photo is visible and the sweeping velocity + // exceed the threshold, switch to next / previous image + PositionController controller = mPositionController; + if (isAlmostEquals(controller.mCurrentScale, controller.mScaleMin)) { + if (velocity < -SWIPE_THRESHOLD) { + stopCurrentSwipingIfNeeded(); + if (next.isEnabled()) { + mTransitionMode = TRANS_SWITCH_NEXT; + controller.startHorizontalSlide(next.mOffsetX - width / 2); + return true; + } + return false; + } + if (velocity > SWIPE_THRESHOLD) { + stopCurrentSwipingIfNeeded(); + if (prev.isEnabled()) { + mTransitionMode = TRANS_SWITCH_PREVIOUS; + controller.startHorizontalSlide(prev.mOffsetX - width / 2); + return true; + } + return false; + } + } + + if (mTransitionMode != TRANS_NONE) return false; + + // Decide whether to swiping to the next/prev image in the zoom-in case + RectF bounds = getImageBounds(); + int left = Math.round(bounds.left); + int right = Math.round(bounds.right); + int threshold = SWITCH_THRESHOLD + gapToSide(right - left, width); + + // If we have moved the picture a lot, switching. + if (next.isEnabled() && threshold < width - right) { + mTransitionMode = TRANS_SWITCH_NEXT; + controller.startHorizontalSlide(next.mOffsetX - width / 2); + return true; + } + if (prev.isEnabled() && threshold < left) { + mTransitionMode = TRANS_SWITCH_PREVIOUS; + controller.startHorizontalSlide(prev.mOffsetX - width / 2); + return true; + } + + return false; + } + + private boolean mIgnoreUpEvent = false; + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onScroll( + MotionEvent e1, MotionEvent e2, float dx, float dy) { + if (mTransitionMode != TRANS_NONE) return true; + mPositionController.scrollBy( + dx, dy, PositionController.ANIM_KIND_SCROLL); + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (mPhotoTapListener != null) { + mPhotoTapListener.onSingleTapUp((int) e.getX(), (int) e.getY()); + } + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + mIgnoreUpEvent = true; + if (!swipeImages(velocityX) && mTransitionMode == TRANS_NONE) { + mPositionController.up(); + } + return true; + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + if (mTransitionMode != TRANS_NONE) return true; + PositionController controller = mPositionController; + float scale = controller.mCurrentScale; + // onDoubleTap happened on the second ACTION_DOWN. + // We need to ignore the next UP event. + mIgnoreUpEvent = true; + if (scale <= 1.0f || isAlmostEquals(scale, controller.mScaleMin)) { + controller.zoomIn( + e.getX(), e.getY(), Math.max(1.5f, scale * 1.5f)); + } else { + controller.resetToFullView(); + } + return true; + } + } + + private class MyScaleListener + extends ScaleGestureDetector.SimpleOnScaleGestureListener { + + @Override + public boolean onScale(ScaleGestureDetector detector) { + float scale = detector.getScaleFactor(); + if (Float.isNaN(scale) || Float.isInfinite(scale) + || mTransitionMode != TRANS_NONE) return true; + mPositionController.scaleBy(scale, + detector.getFocusX(), detector.getFocusY()); + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + if (mTransitionMode != TRANS_NONE) return false; + mPositionController.beginScale( + detector.getFocusX(), detector.getFocusY()); + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mPositionController.endScale(); + swipeImages(0); + } + } + + public void notifyOnNewImage() { + mPositionController.setImageSize(0, 0); + } + + public void startSlideInAnimation(int direction) { + PositionController a = mPositionController; + a.stopAnimation(); + switch (direction) { + case TRANS_SLIDE_IN_LEFT: { + mTransitionMode = TRANS_SLIDE_IN_LEFT; + a.startSlideInAnimation(a.mViewW); + break; + } + case TRANS_SLIDE_IN_RIGHT: { + mTransitionMode = TRANS_SLIDE_IN_RIGHT; + a.startSlideInAnimation(-a.mViewW); + break; + } + default: throw new IllegalArgumentException(String.valueOf(direction)); + } + } + + private class MyDownUpListener implements DownUpDetector.DownUpListener { + public void onDown(MotionEvent e) { + } + + public void onUp(MotionEvent e) { + if (mIgnoreUpEvent) { + mIgnoreUpEvent = false; + return; + } + if (!swipeImages(0) && mTransitionMode == TRANS_NONE) { + mPositionController.up(); + } + } + } + + private void switchToNextImage() { + // We update the texture here directly to prevent texture uploading. + ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS]; + ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT]; + mTileView.invalidateTiles(); + if (prevNail.mTexture != null) prevNail.mTexture.recycle(); + prevNail.mTexture = mTileView.mBackupImage; + mTileView.mBackupImage = nextNail.mTexture; + nextNail.mTexture = null; + mModel.next(); + } + + private void switchToPreviousImage() { + // We update the texture here directly to prevent texture uploading. + ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS]; + ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT]; + mTileView.invalidateTiles(); + if (nextNail.mTexture != null) nextNail.mTexture.recycle(); + nextNail.mTexture = mTileView.mBackupImage; + mTileView.mBackupImage = prevNail.mTexture; + nextNail.mTexture = null; + mModel.previous(); + } + + private void onTransitionComplete() { + int mode = mTransitionMode; + mTransitionMode = TRANS_NONE; + + if (mModel == null) return; + if (mode == TRANS_SWITCH_NEXT) { + switchToNextImage(); + } else if (mode == TRANS_SWITCH_PREVIOUS) { + switchToPreviousImage(); + } + } + + private boolean isDown() { + return mDownUpDetector.isDown(); + } + + public static interface Model extends TileImageView.Model { + public void next(); + public void previous(); + public int getImageRotation(); + + // Return null if the specified image is unavailable. + public ImageData getNextImage(); + public ImageData getPreviousImage(); + } + + public static class ImageData { + public int rotation; + public Bitmap bitmap; + + public ImageData(Bitmap bitmap, int rotation) { + this.bitmap = bitmap; + this.rotation = rotation; + } + } + + private static int getRotated(int degree, int original, int theother) { + return ((degree / 90) & 1) == 0 ? original : theother; + } + + private class ScreenNailEntry { + private boolean mVisible; + private boolean mEnabled; + + private int mRotation; + private int mDrawWidth; + private int mDrawHeight; + private int mOffsetX; + + private BitmapTexture mTexture; + + public void set(boolean enabled, Bitmap bitmap, int rotation) { + mEnabled = enabled; + mRotation = rotation; + if (bitmap == null) { + if (mTexture != null) mTexture.recycle(); + mTexture = null; + } else { + if (mTexture != null) { + if (mTexture.getBitmap() != bitmap) { + mTexture.recycle(); + mTexture = new BitmapTexture(bitmap); + } + } else { + mTexture = new BitmapTexture(bitmap); + } + updateDrawingSize(); + } + } + + public void layoutRightEdgeAt(int x) { + mVisible = x > 0; + mOffsetX = x - getRotated( + mRotation, mDrawWidth, mDrawHeight) / 2; + } + + public void layoutLeftEdgeAt(int x) { + mVisible = x < getWidth(); + mOffsetX = x + getRotated( + mRotation, mDrawWidth, mDrawHeight) / 2; + } + + public int gapToSide() { + return ((mRotation / 90) & 1) != 0 + ? PhotoView.gapToSide(mDrawHeight, getWidth()) + : PhotoView.gapToSide(mDrawWidth, getWidth()); + } + + public void updateDrawingSize() { + if (mTexture == null) return; + + int width = mTexture.getWidth(); + int height = mTexture.getHeight(); + float s = mPositionController.getMinimalScale(width, height, mRotation); + mDrawWidth = Math.round(width * s); + mDrawHeight = Math.round(height * s); + } + + public boolean isEnabled() { + return mEnabled; + } + + public void draw(GLCanvas canvas) { + int x = mOffsetX; + int y = getHeight() / 2; + + if (mTexture != null) { + if (mRotation != 0) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + canvas.translate(x, y, 0); + canvas.rotate(mRotation, 0, 0, 1); //mRotation + canvas.translate(-x, -y, 0); + } + mTexture.draw(canvas, x - mDrawWidth / 2, y - mDrawHeight / 2, + mDrawWidth, mDrawHeight); + if (mRotation != 0) { + canvas.restore(); + } + } + } + } + + public void pause() { + mPositionController.skipAnimation(); + mTransitionMode = TRANS_NONE; + mTileView.freeTextures(); + for (ScreenNailEntry entry : mScreenNails) { + entry.set(false, null, 0); + } + } + + public void resume() { + mTileView.prepareTextures(); + } + + public void setOpenedItem(Path itemPath) { + mOpenedItemPath = itemPath; + } + + public void showVideoPlayIcon(boolean show) { + mShowVideoPlayIcon = show; + } +} diff --git a/src/com/android/gallery3d/ui/PositionProvider.java b/src/com/android/gallery3d/ui/PositionProvider.java new file mode 100644 index 000000000..930c61ee9 --- /dev/null +++ b/src/com/android/gallery3d/ui/PositionProvider.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.ui.PositionRepository.Position; + +public interface PositionProvider { + public Position getPosition(long identity, Position target); +} diff --git a/src/com/android/gallery3d/ui/PositionRepository.java b/src/com/android/gallery3d/ui/PositionRepository.java new file mode 100644 index 000000000..0b829fa25 --- /dev/null +++ b/src/com/android/gallery3d/ui/PositionRepository.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; + +import java.util.HashMap; +import java.util.WeakHashMap; + +public class PositionRepository { + private static final WeakHashMap + sMap = new WeakHashMap(); + + public static class Position implements Cloneable { + public float x; + public float y; + public float z; + public float theta; + public float alpha; + + public Position() { + } + + public Position(float x, float y, float z) { + this(x, y, z, 0f, 1f); + } + + public Position(float x, float y, float z, float ftheta, float alpha) { + this.x = x; + this.y = y; + this.z = z; + this.theta = ftheta; + this.alpha = alpha; + } + + @Override + public Position clone() { + try { + return (Position) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(); // we do support clone. + } + } + + public void set(Position another) { + x = another.x; + y = another.y; + z = another.z; + theta = another.theta; + alpha = another.alpha; + } + + public void set(float x, float y, float z, float ftheta, float alpha) { + this.x = x; + this.y = y; + this.z = z; + this.theta = ftheta; + this.alpha = alpha; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof Position)) return false; + Position position = (Position) object; + return x == position.x && y == position.y && z == position.z + && theta == position.theta + && alpha == position.alpha; + } + + public static void interpolate( + Position source, Position target, Position output, float progress) { + if (progress < 1f) { + output.set( + Utils.interpolateScale(source.x, target.x, progress), + Utils.interpolateScale(source.y, target.y, progress), + Utils.interpolateScale(source.z, target.z, progress), + Utils.interpolateAngle(source.theta, target.theta, progress), + Utils.interpolateScale(source.alpha, target.alpha, progress)); + } else { + output.set(target); + } + } + } + + public static PositionRepository getInstance(GalleryActivity activity) { + PositionRepository repository = sMap.get(activity); + if (repository == null) { + repository = new PositionRepository(); + sMap.put(activity, repository); + } + return repository; + } + + private HashMap mData = new HashMap(); + private int mOffsetX; + private int mOffsetY; + private Position mTempPosition = new Position(); + + public Position get(Long identity) { + Position position = mData.get(identity); + if (position == null) return null; + mTempPosition.set(position); + position = mTempPosition; + position.x -= mOffsetX; + position.y -= mOffsetY; + return position; + } + + public void setOffset(int offsetX, int offsetY) { + mOffsetX = offsetX; + mOffsetY = offsetY; + } + + public void putPosition(Long identity, Position position) { + Position clone = position.clone(); + clone.x += mOffsetX; + clone.y += mOffsetY; + mData.put(identity, clone); + } + + public void clear() { + mData.clear(); + } +} diff --git a/src/com/android/gallery3d/ui/ProgressBar.java b/src/com/android/gallery3d/ui/ProgressBar.java new file mode 100644 index 000000000..c62fa9a62 --- /dev/null +++ b/src/com/android/gallery3d/ui/ProgressBar.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.graphics.Rect; + +public class ProgressBar extends GLView { + private final int MAX_PROGRESS = 10000; + private int mProgress; + private int mSecondaryProgress; + private BasicTexture mProgressTexture; + private BasicTexture mSecondaryProgressTexture; + private BasicTexture mBackgrondTexture; + + + public ProgressBar(Context context, int resProgress, + int resSecondaryProgress, int resBackground) { + mProgressTexture = new NinePatchTexture(context, resProgress); + mSecondaryProgressTexture = new NinePatchTexture( + context, resSecondaryProgress); + mBackgrondTexture = new NinePatchTexture(context, resBackground); + + } + + // The progress value is between 0 (empty) and MAX_PROGRESS (full). + public void setProgress(int progress) { + mProgress = progress; + } + + public void setSecondaryProgress(int progress) { + mSecondaryProgress = progress; + } + + @Override + protected void render(GLCanvas canvas) { + Rect p = mPaddings; + + int width = getWidth() - p.left - p.right; + int height = getHeight() - p.top - p.bottom; + + int primary = width * mProgress / MAX_PROGRESS; + int secondary = width * mSecondaryProgress / MAX_PROGRESS; + int x = p.left; + int y = p.top; + + canvas.drawTexture(mBackgrondTexture, x, y, width, height); + canvas.drawTexture(mProgressTexture, x, y, primary, height); + canvas.drawTexture(mSecondaryProgressTexture, x, y, secondary, height); + } +} diff --git a/src/com/android/gallery3d/ui/ProgressSpinner.java b/src/com/android/gallery3d/ui/ProgressSpinner.java new file mode 100644 index 000000000..e4d60242b --- /dev/null +++ b/src/com/android/gallery3d/ui/ProgressSpinner.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; + +import android.content.Context; + +public class ProgressSpinner { + private static float ROTATE_SPEED_OUTER = 1080f / 3500f; + private static float ROTATE_SPEED_INNER = -720f / 3500f; + private final ResourceTexture mOuter; + private final ResourceTexture mInner; + private final int mWidth; + private final int mHeight; + + private float mInnerDegree = 0f; + private float mOuterDegree = 0f; + private long mAnimationTimestamp = -1; + + public ProgressSpinner(Context context) { + mOuter = new ResourceTexture(context, R.drawable.spinner_76_outer_holo); + mInner = new ResourceTexture(context, R.drawable.spinner_76_inner_holo); + + mWidth = Math.max(mOuter.getWidth(), mInner.getWidth()); + mHeight = Math.max(mOuter.getHeight(), mInner.getHeight()); + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + public void startAnimation() { + mAnimationTimestamp = -1; + mOuterDegree = 0; + mInnerDegree = 0; + } + + public void draw(GLCanvas canvas, int x, int y) { + long now = canvas.currentAnimationTimeMillis(); + if (mAnimationTimestamp == -1) mAnimationTimestamp = now; + mOuterDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_OUTER; + mInnerDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_INNER; + + mAnimationTimestamp = now; + + // just preventing overflow + if (mOuterDegree > 360) mOuterDegree -= 360f; + if (mInnerDegree < 0) mInnerDegree += 360f; + + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + + canvas.translate(x + mWidth / 2, y + mHeight / 2, 0); + canvas.rotate(mInnerDegree, 0, 0, 1); + mOuter.draw(canvas, -mOuter.getWidth() / 2, -mOuter.getHeight() / 2); + canvas.rotate(mOuterDegree - mInnerDegree, 0, 0, 1); + mInner.draw(canvas, -mInner.getWidth() / 2, -mInner.getHeight() / 2); + canvas.restore(); + } +} diff --git a/src/com/android/gallery3d/ui/RawTexture.java b/src/com/android/gallery3d/ui/RawTexture.java new file mode 100644 index 000000000..c1be435d1 --- /dev/null +++ b/src/com/android/gallery3d/ui/RawTexture.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import javax.microedition.khronos.opengles.GL11; + +// RawTexture is used for texture created by glCopyTexImage2D. +// +// It will throw RuntimeException in onBind() if used with a different GL +// context. It is only used internally by copyTexture() in GLCanvas. +class RawTexture extends BasicTexture { + + private RawTexture(GLCanvas canvas, int id) { + super(canvas, id, STATE_LOADED); + } + + public static RawTexture newInstance(GLCanvas canvas) { + int[] textureId = new int[1]; + GL11 gl = canvas.getGLInstance(); + gl.glGenTextures(1, textureId, 0); + return new RawTexture(canvas, textureId[0]); + } + + @Override + protected boolean onBind(GLCanvas canvas) { + if (mCanvasRef.get() != canvas) { + throw new RuntimeException("cannot bind to different canvas"); + } + return true; + } + + public boolean isOpaque() { + return true; + } + + @Override + public void yield() { + // we cannot free the texture because we have no backup. + } +} diff --git a/src/com/android/gallery3d/ui/ResourceTexture.java b/src/com/android/gallery3d/ui/ResourceTexture.java new file mode 100644 index 000000000..08fb89187 --- /dev/null +++ b/src/com/android/gallery3d/ui/ResourceTexture.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.common.Utils; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +// ResourceTexture is a texture whose Bitmap is decoded from a resource. +// By default ResourceTexture is not opaque. +public class ResourceTexture extends UploadedTexture { + + protected final Context mContext; + protected final int mResId; + + public ResourceTexture(Context context, int resId) { + mContext = Utils.checkNotNull(context); + mResId = resId; + setOpaque(false); + } + + @Override + protected Bitmap onGetBitmap() { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + return BitmapFactory.decodeResource( + mContext.getResources(), mResId, options); + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + if (!inFinalizer()) { + bitmap.recycle(); + } + } +} diff --git a/src/com/android/gallery3d/ui/ScrollBarView.java b/src/com/android/gallery3d/ui/ScrollBarView.java new file mode 100644 index 000000000..7e375c9f7 --- /dev/null +++ b/src/com/android/gallery3d/ui/ScrollBarView.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; + +import android.content.Context; +import android.graphics.Rect; + +public class ScrollBarView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "ScrollBarView"; + + public interface Listener { + void onScrollBarPositionChanged(int position); + } + + private int mBarHeight; + + private int mGripHeight; + private int mGripPosition; // left side of the grip + private int mGripWidth; // zero if the grip is disabled + private int mGivenGripWidth; + + private int mContentPosition; + private int mContentTotal; + + private Listener mListener; + private NinePatchTexture mScrollBarTexture; + + public ScrollBarView(Context context, int gripHeight, int gripWidth) { + mScrollBarTexture = new NinePatchTexture( + context, R.drawable.scrollbar_handle_holo_dark); + mGripPosition = 0; + mGripWidth = 0; + mGivenGripWidth = gripWidth; + mGripHeight = gripHeight; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + if (!changed) return; + mBarHeight = bottom - top; + } + + // The content position is between 0 to "total". The current position is + // in "position". + public void setContentPosition(int position, int total) { + if (position == mContentPosition && total == mContentTotal) { + return; + } + + invalidate(); + + mContentPosition = position; + mContentTotal = total; + + // If the grip cannot move, don't draw it. + if (mContentTotal <= 0) { + mGripPosition = 0; + mGripWidth = 0; + return; + } + + // Map from the content range to scroll bar range. + // + // mContentTotal --> getWidth() - mGripWidth + // mContentPosition --> mGripPosition + mGripWidth = mGivenGripWidth; + float r = (getWidth() - mGripWidth) / (float) mContentTotal; + mGripPosition = Math.round(r * mContentPosition); + } + + private void notifyContentPositionFromGrip() { + if (mContentTotal <= 0) return; + float r = (getWidth() - mGripWidth) / (float) mContentTotal; + int newContentPosition = Math.round(mGripPosition / r); + mListener.onScrollBarPositionChanged(newContentPosition); + } + + @Override + protected void render(GLCanvas canvas) { + super.render(canvas); + if (mGripWidth == 0) return; + Rect b = bounds(); + int y = (mBarHeight - mGripHeight) / 2; + mScrollBarTexture.draw(canvas, mGripPosition, y, mGripWidth, mGripHeight); + } + + // The onTouch() handler is disabled because now we don't want the user + // to drag the bar (it's an indicator only). + /* + @Override + protected boolean onTouch(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + int x = (int) event.getX(); + return (x >= mGripPosition && x < mGripPosition + mGripWidth); + } + case MotionEvent.ACTION_MOVE: { + // Adjust x by mGripWidth / 2 so the center of the grip + // matches the touch position. + int x = (int) event.getX() - mGripWidth / 2; + x = Utils.clamp(x, 0, getWidth() - mGripWidth); + if (mGripPosition != x) { + mGripPosition = x; + notifyContentPositionFromGrip(); + invalidate(); + } + break; + } + } + return true; + } + */ +} diff --git a/src/com/android/gallery3d/ui/ScrollView.java b/src/com/android/gallery3d/ui/ScrollView.java new file mode 100644 index 000000000..f7628335c --- /dev/null +++ b/src/com/android/gallery3d/ui/ScrollView.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; + +import android.content.Context; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View.MeasureSpec; + +// The current implementation can only scroll vertically. +public class ScrollView extends GLView { + + private static final int MIN_SCROLLER_HEIGHT = 20; + + private NinePatchTexture mScroller; + private int mScrollLimit = 0; + private int mScrollerHeight = MIN_SCROLLER_HEIGHT; + private GestureDetector mGestureDetector; + + public ScrollView(Context context) { + mScroller = new NinePatchTexture(context, R.drawable.scrollbar_handle_holo_dark); + mGestureDetector = new GestureDetector(context, new MyGestureListener()); + } + + private GLView getContentView() { + return getComponentCount() == 0 ? null : getComponent(0); + } + + @Override + public void onLayout(boolean sizeChange, int l, int t, int r, int b) { + GLView content = getContentView(); + int width = getWidth(); + int height = getHeight(); + content.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.UNSPECIFIED); + int contentHeight = content.getMeasuredHeight(); + content.layout(0, 0, width, contentHeight); + if (height < contentHeight) { + mScrollLimit = contentHeight - height; + mScrollerHeight = Math.max(MIN_SCROLLER_HEIGHT, + height * height / contentHeight); + } else { + mScrollLimit = 0; + } + mScrollY = Utils.clamp(mScrollY, 0, mScrollLimit); + } + + @Override + public void render(GLCanvas canvas) { + GLView content = getContentView(); + if (content == null) return; + int width = getWidth(); + int height = getHeight(); + + canvas.save(GLCanvas.SAVE_FLAG_CLIP); + canvas.clipRect(0, 0, width, height); + super.render(canvas); + if (mScrollLimit > 0) { + int x = getWidth() - mScroller.getWidth(); + int y = (height - mScrollerHeight) * mScrollY / mScrollLimit; + mScroller.draw(canvas, x, y, mScroller.getWidth(), mScrollerHeight); + } + canvas.restore(); + } + + @Override + public boolean onTouch(MotionEvent event) { + mGestureDetector.onTouchEvent(event); + return true; + } + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onScroll(MotionEvent e1, + MotionEvent e2, float distanceX, float distanceY) { + mScrollY = Utils.clamp(mScrollY + (int) distanceY, 0, mScrollLimit); + invalidate(); + return true; + } + } +} diff --git a/src/com/android/gallery3d/ui/ScrollerHelper.java b/src/com/android/gallery3d/ui/ScrollerHelper.java new file mode 100644 index 000000000..9f19cec96 --- /dev/null +++ b/src/com/android/gallery3d/ui/ScrollerHelper.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.common.Utils; + +import android.content.Context; +import android.view.ViewConfiguration; +import android.widget.OverScroller; + +public class ScrollerHelper { + private OverScroller mScroller; + private int mOverflingDistance; + private boolean mOverflingEnabled; + + public ScrollerHelper(Context context) { + mScroller = new OverScroller(context); + ViewConfiguration configuration = ViewConfiguration.get(context); + mOverflingDistance = configuration.getScaledOverflingDistance(); + } + + public void setOverfling(boolean enabled) { + mOverflingEnabled = enabled; + } + + /** + * Call this when you want to know the new location. The position will be + * updated and can be obtained by getPosition(). Returns true if the + * animation is not yet finished. + */ + public boolean advanceAnimation(long currentTimeMillis) { + return mScroller.computeScrollOffset(); + } + + public boolean isFinished() { + return mScroller.isFinished(); + } + + public void forceFinished() { + mScroller.forceFinished(true); + } + + public int getPosition() { + return mScroller.getCurrX(); + } + + public void setPosition(int position) { + mScroller.startScroll( + position, 0, // startX, startY + 0, 0, 0); // dx, dy, duration + + // This forces the scroller to reach the final position. + mScroller.abortAnimation(); + } + + public void fling(int velocity, int min, int max) { + int currX = getPosition(); + mScroller.fling( + currX, 0, // startX, startY + velocity, 0, // velocityX, velocityY + min, max, // minX, maxX + 0, 0, // minY, maxY + mOverflingEnabled ? mOverflingDistance : 0, 0); + } + + public boolean startScroll(int distance, int min, int max) { + int currPosition = mScroller.getCurrX(); + int finalPosition = mScroller.getFinalX(); + int newPosition = Utils.clamp(finalPosition + distance, min, max); + if (newPosition != currPosition) { + mScroller.startScroll( + currPosition, 0, // startX, startY + newPosition - currPosition, 0, 0); // dx, dy, duration + return true; + } else { + return false; + } + } +} diff --git a/src/com/android/gallery3d/ui/SelectionDrawer.java b/src/com/android/gallery3d/ui/SelectionDrawer.java new file mode 100644 index 000000000..2655a221c --- /dev/null +++ b/src/com/android/gallery3d/ui/SelectionDrawer.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.Path; + +import android.graphics.Rect; + +/** + * Drawer class responsible for drawing selectable frame. + */ +public abstract class SelectionDrawer { + public static final int DATASOURCE_TYPE_NOT_CATEGORIZED = 0; + public static final int DATASOURCE_TYPE_LOCAL = 1; + public static final int DATASOURCE_TYPE_PICASA = 2; + public static final int DATASOURCE_TYPE_MTP = 3; + public static final int DATASOURCE_TYPE_CAMERA = 4; + + public abstract void prepareDrawing(); + public abstract void draw(GLCanvas canvas, Texture content, + int width, int height, int rotation, Path path, + int topIndex, int dataSourceType, int mediaType, + boolean wantCache, boolean isCaching); + public abstract void drawFocus(GLCanvas canvas, int width, int height); + + public void draw(GLCanvas canvas, Texture content, int width, int height, + int rotation, Path path, int mediaType) { + draw(canvas, content, width, height, rotation, path, 0, + DATASOURCE_TYPE_NOT_CATEGORIZED, mediaType, + false, false); + } + + public static void drawWithRotation(GLCanvas canvas, Texture content, + int x, int y, int width, int height, int rotation) { + if (rotation != 0) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + canvas.rotate(rotation, 0, 0, 1); + } + + content.draw(canvas, x, y, width, height); + + if (rotation != 0) { + canvas.restore(); + } + } + + public static void drawWithRotationAndGray(GLCanvas canvas, Texture content, + int x, int y, int width, int height, int rotation, + int topIndex) { + if (rotation != 0) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + canvas.rotate(rotation, 0, 0, 1); + } + + if (topIndex > 0 && (content instanceof BasicTexture)) { + float ratio = Utils.clamp(0.3f + 0.2f * topIndex, 0f, 1f); + canvas.drawMixed((BasicTexture) content, 0xFF222222, ratio, + x, y, width, height); + } else { + content.draw(canvas, x, y, width, height); + } + + if (rotation != 0) { + canvas.restore(); + } + } + + public static void drawFrame(GLCanvas canvas, NinePatchTexture frame, + int x, int y, int width, int height) { + Rect p = frame.getPaddings(); + frame.draw(canvas, x - p.left, y - p.top, width + p.left + p.right, + height + p.top + p.bottom); + } +} diff --git a/src/com/android/gallery3d/ui/SelectionManager.java b/src/com/android/gallery3d/ui/SelectionManager.java new file mode 100644 index 000000000..b85ca7a41 --- /dev/null +++ b/src/com/android/gallery3d/ui/SelectionManager.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.app.GalleryContext; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; + +import android.content.Context; +import android.os.Vibrator; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +public class SelectionManager { + @SuppressWarnings("unused") + private static final String TAG = "SelectionManager"; + + public static final int ENTER_SELECTION_MODE = 1; + public static final int LEAVE_SELECTION_MODE = 2; + public static final int SELECT_ALL_MODE = 3; + + private Set mClickedSet; + private MediaSet mSourceMediaSet; + private final Vibrator mVibrator; + private SelectionListener mListener; + private DataManager mDataManager; + private boolean mInverseSelection; + private boolean mIsAlbumSet; + private boolean mInSelectionMode; + private boolean mAutoLeave = true; + private int mTotal; + + public interface SelectionListener { + public void onSelectionModeChange(int mode); + public void onSelectionChange(Path path, boolean selected); + } + + public SelectionManager(GalleryContext galleryContext, boolean isAlbumSet) { + Context context = galleryContext.getAndroidContext(); + mDataManager = galleryContext.getDataManager(); + mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + mClickedSet = new HashSet(); + mIsAlbumSet = isAlbumSet; + mTotal = -1; + } + + // Whether we will leave selection mode automatically once the number of + // selected items is down to zero. + public void setAutoLeaveSelectionMode(boolean enable) { + mAutoLeave = enable; + } + + public void setSelectionListener(SelectionListener listener) { + mListener = listener; + } + + public void selectAll() { + enterSelectionMode(); + mInverseSelection = true; + mClickedSet.clear(); + if (mListener != null) mListener.onSelectionModeChange(SELECT_ALL_MODE); + } + + public void deSelectAll() { + leaveSelectionMode(); + mInverseSelection = false; + mClickedSet.clear(); + } + + public boolean inSelectAllMode() { + return mInverseSelection; + } + + public boolean inSelectionMode() { + return mInSelectionMode; + } + + public void enterSelectionMode() { + if (mInSelectionMode) return; + + mInSelectionMode = true; + mVibrator.vibrate(100); + if (mListener != null) mListener.onSelectionModeChange(ENTER_SELECTION_MODE); + } + + public void leaveSelectionMode() { + if (!mInSelectionMode) return; + + mInSelectionMode = false; + mInverseSelection = false; + mClickedSet.clear(); + if (mListener != null) mListener.onSelectionModeChange(LEAVE_SELECTION_MODE); + } + + public boolean isItemSelected(Path itemId) { + return mInverseSelection ^ mClickedSet.contains(itemId); + } + + public int getSelectedCount() { + int count = mClickedSet.size(); + if (mInverseSelection) { + if (mTotal < 0) { + mTotal = mIsAlbumSet + ? mSourceMediaSet.getSubMediaSetCount() + : mSourceMediaSet.getMediaItemCount(); + } + count = mTotal - count; + } + return count; + } + + public void toggle(Path path) { + if (mClickedSet.contains(path)) { + mClickedSet.remove(path); + } else { + enterSelectionMode(); + mClickedSet.add(path); + } + + if (mListener != null) mListener.onSelectionChange(path, isItemSelected(path)); + if (getSelectedCount() == 0 && mAutoLeave) { + leaveSelectionMode(); + } + } + + private static void expandMediaSet(ArrayList items, MediaSet set) { + int subCount = set.getSubMediaSetCount(); + for (int i = 0; i < subCount; i++) { + expandMediaSet(items, set.getSubMediaSet(i)); + } + int total = set.getMediaItemCount(); + int batch = 50; + int index = 0; + + while (index < total) { + int count = index + batch < total + ? batch + : total - index; + ArrayList list = set.getMediaItem(index, count); + for (MediaItem item : list) { + items.add(item.getPath()); + } + index += batch; + } + } + + public ArrayList getSelected(boolean expandSet) { + ArrayList selected = new ArrayList(); + if (mIsAlbumSet) { + if (mInverseSelection) { + int max = mSourceMediaSet.getSubMediaSetCount(); + for (int i = 0; i < max; i++) { + MediaSet set = mSourceMediaSet.getSubMediaSet(i); + Path id = set.getPath(); + if (!mClickedSet.contains(id)) { + if (expandSet) { + expandMediaSet(selected, set); + } else { + selected.add(id); + } + } + } + } else { + for (Path id : mClickedSet) { + if (expandSet) { + expandMediaSet(selected, mDataManager.getMediaSet(id)); + } else { + selected.add(id); + } + } + } + } else { + if (mInverseSelection) { + + int total = mSourceMediaSet.getMediaItemCount(); + int index = 0; + while (index < total) { + int count = Math.min(total - index, MediaSet.MEDIAITEM_BATCH_FETCH_COUNT); + ArrayList list = mSourceMediaSet.getMediaItem(index, count); + for (MediaItem item : list) { + Path id = item.getPath(); + if (!mClickedSet.contains(id)) selected.add(id); + } + index += count; + } + } else { + for (Path id : mClickedSet) { + selected.add(id); + } + } + } + return selected; + } + + public void setSourceMediaSet(MediaSet set) { + mSourceMediaSet = set; + mTotal = -1; + } + + public MediaSet getSourceMediaSet() { + return mSourceMediaSet; + } +} diff --git a/src/com/android/gallery3d/ui/SlideshowView.java b/src/com/android/gallery3d/ui/SlideshowView.java new file mode 100644 index 000000000..79a6bf080 --- /dev/null +++ b/src/com/android/gallery3d/ui/SlideshowView.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.anim.CanvasAnimation; +import com.android.gallery3d.anim.FloatAnimation; + +import android.graphics.Bitmap; +import android.graphics.PointF; + +import java.util.Random; +import javax.microedition.khronos.opengles.GL11; + +public class SlideshowView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "SlideshowView"; + + private static final int SLIDESHOW_DURATION = 3500; + private static final int TRANSITION_DURATION = 1000; + + private static final float SCALE_SPEED = 0.20f ; + private static final float MOVE_SPEED = SCALE_SPEED; + + private int mCurrentRotation; + private BitmapTexture mCurrentTexture; + private SlideshowAnimation mCurrentAnimation; + + private int mPrevRotation; + private BitmapTexture mPrevTexture; + private SlideshowAnimation mPrevAnimation; + + private final FloatAnimation mTransitionAnimation = + new FloatAnimation(0, 1, TRANSITION_DURATION); + + private Random mRandom = new Random(); + + public void next(Bitmap bitmap, int rotation) { + + mTransitionAnimation.start(); + + if (mPrevTexture != null) { + mPrevTexture.getBitmap().recycle(); + mPrevTexture.recycle(); + } + + mPrevTexture = mCurrentTexture; + mPrevAnimation = mCurrentAnimation; + mPrevRotation = mCurrentRotation; + + mCurrentRotation = rotation; + mCurrentTexture = new BitmapTexture(bitmap); + if (((rotation / 90) & 0x01) == 0) { + mCurrentAnimation = new SlideshowAnimation( + mCurrentTexture.getWidth(), mCurrentTexture.getHeight(), + mRandom); + } else { + mCurrentAnimation = new SlideshowAnimation( + mCurrentTexture.getHeight(), mCurrentTexture.getWidth(), + mRandom); + } + mCurrentAnimation.start(); + + invalidate(); + } + + public void release() { + if (mPrevTexture != null) { + mPrevTexture.recycle(); + mPrevTexture = null; + } + if (mCurrentTexture != null) { + mCurrentTexture.recycle(); + mCurrentTexture = null; + } + } + + @Override + protected void render(GLCanvas canvas) { + long currentTimeMillis = canvas.currentAnimationTimeMillis(); + boolean requestRender = mTransitionAnimation.calculate(currentTimeMillis); + GL11 gl = canvas.getGLInstance(); + gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE); + float alpha = mPrevTexture == null ? 1f : mTransitionAnimation.get(); + + if (mPrevTexture != null && alpha != 1f) { + requestRender |= mPrevAnimation.calculate(currentTimeMillis); + canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX); + canvas.setAlpha(1f - alpha); + mPrevAnimation.apply(canvas); + canvas.rotate(mPrevRotation, 0, 0, 1); + mPrevTexture.draw(canvas, -mPrevTexture.getWidth() / 2, + -mPrevTexture.getHeight() / 2); + canvas.restore(); + } + if (mCurrentTexture != null) { + requestRender |= mCurrentAnimation.calculate(currentTimeMillis); + canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX); + canvas.setAlpha(alpha); + mCurrentAnimation.apply(canvas); + canvas.rotate(mCurrentRotation, 0, 0, 1); + mCurrentTexture.draw(canvas, -mCurrentTexture.getWidth() / 2, + -mCurrentTexture.getHeight() / 2); + canvas.restore(); + } + if (requestRender) invalidate(); + gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); + } + + private class SlideshowAnimation extends CanvasAnimation { + private final int mWidth; + private final int mHeight; + + private final PointF mMovingVector; + private float mProgress; + + public SlideshowAnimation(int width, int height, Random random) { + mWidth = width; + mHeight = height; + mMovingVector = new PointF( + MOVE_SPEED * mWidth * (random.nextFloat() - 0.5f), + MOVE_SPEED * mHeight * (random.nextFloat() - 0.5f)); + setDuration(SLIDESHOW_DURATION); + } + + @Override + public void apply(GLCanvas canvas) { + int viewWidth = getWidth(); + int viewHeight = getHeight(); + + float initScale = Math.min(2f, Math.min((float) + viewWidth / mWidth, (float) viewHeight / mHeight)); + float scale = initScale * (1 + SCALE_SPEED * mProgress); + + float centerX = viewWidth / 2 + mMovingVector.x * mProgress; + float centerY = viewHeight / 2 + mMovingVector.y * mProgress; + + canvas.translate(centerX, centerY, 0); + canvas.scale(scale, scale, 0); + } + + @Override + public int getCanvasSaveFlags() { + return GLCanvas.SAVE_FLAG_MATRIX; + } + + @Override + protected void onCalculate(float progress) { + mProgress = progress; + } + } +} diff --git a/src/com/android/gallery3d/ui/SlotView.java b/src/com/android/gallery3d/ui/SlotView.java new file mode 100644 index 000000000..a8ca5f290 --- /dev/null +++ b/src/com/android/gallery3d/ui/SlotView.java @@ -0,0 +1,607 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.anim.Animation; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.ui.PositionRepository.Position; +import com.android.gallery3d.util.LinkedNode; + +import android.content.Context; +import android.graphics.Rect; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.animation.DecelerateInterpolator; + +import java.util.ArrayList; +import java.util.HashMap; + +public class SlotView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "SlotView"; + + private static final boolean WIDE = true; + + private static final int INDEX_NONE = -1; + + public interface Listener { + public void onSingleTapUp(int index); + public void onLongTap(int index); + public void onScrollPositionChanged(int position, int total); + } + + public static class SimpleListener implements Listener { + public void onSingleTapUp(int index) {} + public void onLongTap(int index) {} + public void onScrollPositionChanged(int position, int total) {} + } + + private final GestureDetector mGestureDetector; + private final ScrollerHelper mScroller; + private final Paper mPaper = new Paper(); + + private Listener mListener; + private UserInteractionListener mUIListener; + + // Use linked hash map to keep the rendering order + private HashMap mItems = + new HashMap(); + + public LinkedNode.List mItemList = LinkedNode.newList(); + + // This is used for multipass rendering + private ArrayList mCurrentItems = new ArrayList(); + private ArrayList mNextItems = new ArrayList(); + + private boolean mMoreAnimation = false; + private MyAnimation mAnimation = null; + private final Position mTempPosition = new Position(); + private final Layout mLayout = new Layout(); + private PositionProvider mPositions; + private int mStartIndex = INDEX_NONE; + + // whether the down action happened while the view is scrolling. + private boolean mDownInScrolling; + private int mOverscrollEffect = OVERSCROLL_3D; + + public static final int OVERSCROLL_3D = 0; + public static final int OVERSCROLL_SYSTEM = 1; + public static final int OVERSCROLL_NONE = 2; + + public SlotView(Context context) { + mGestureDetector = + new GestureDetector(context, new MyGestureListener()); + mScroller = new ScrollerHelper(context); + } + + public void setCenterIndex(int index) { + int slotCount = mLayout.mSlotCount; + if (index < 0 || index >= slotCount) { + return; + } + Rect rect = mLayout.getSlotRect(index); + int position = WIDE + ? (rect.left + rect.right - getWidth()) / 2 + : (rect.top + rect.bottom - getHeight()) / 2; + setScrollPosition(position); + } + + public void makeSlotVisible(int index) { + Rect rect = mLayout.getSlotRect(index); + int visibleBegin = WIDE ? mScrollX : mScrollY; + int visibleLength = WIDE ? getWidth() : getHeight(); + int visibleEnd = visibleBegin + visibleLength; + int slotBegin = WIDE ? rect.left : rect.top; + int slotEnd = WIDE ? rect.right : rect.bottom; + + int position = visibleBegin; + if (visibleLength < slotEnd - slotBegin) { + position = visibleBegin; + } else if (slotBegin < visibleBegin) { + position = slotBegin; + } else if (slotEnd > visibleEnd) { + position = slotEnd - visibleLength; + } + + setScrollPosition(position); + } + + public void setScrollPosition(int position) { + position = Utils.clamp(position, 0, mLayout.getScrollLimit()); + mScroller.setPosition(position); + updateScrollPosition(position, false); + } + + public void setSlotSize(int slotWidth, int slotHeight) { + mLayout.setSlotSize(slotWidth, slotHeight); + } + + @Override + public void addComponent(GLView view) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeComponent(GLView view) { + throw new UnsupportedOperationException(); + } + + @Override + protected void onLayout(boolean changeSize, int l, int t, int r, int b) { + if (!changeSize) return; + mLayout.setSize(r - l, b - t); + onLayoutChanged(r - l, b - t); + if (mOverscrollEffect == OVERSCROLL_3D) { + mPaper.setSize(r - l, b - t); + } + } + + protected void onLayoutChanged(int width, int height) { + } + + public void startTransition(PositionProvider position) { + mPositions = position; + mAnimation = new MyAnimation(); + mAnimation.start(); + if (mItems.size() != 0) invalidate(); + } + + public void savePositions(PositionRepository repository) { + repository.clear(); + LinkedNode.List list = mItemList; + ItemEntry entry = list.getFirst(); + Position position = new Position(); + while (entry != null) { + position.set(entry.target); + position.x -= mScrollX; + position.y -= mScrollY; + repository.putPosition(entry.item.getIdentity(), position); + entry = list.nextOf(entry); + } + } + + private void updateScrollPosition(int position, boolean force) { + if (!force && (WIDE ? position == mScrollX : position == mScrollY)) return; + if (WIDE) { + mScrollX = position; + } else { + mScrollY = position; + } + mLayout.setScrollPosition(position); + onScrollPositionChanged(position); + } + + protected void onScrollPositionChanged(int newPosition) { + int limit = mLayout.getScrollLimit(); + mListener.onScrollPositionChanged(newPosition, limit); + } + + public void putDisplayItem(Position target, Position base, DisplayItem item) { + ItemEntry entry = new ItemEntry(item, target, base); + mItemList.insertLast(entry); + mItems.put(item, entry); + } + + public void removeDisplayItem(DisplayItem item) { + ItemEntry entry = mItems.remove(item); + if (entry != null) entry.remove(); + } + + public Rect getSlotRect(int slotIndex) { + return mLayout.getSlotRect(slotIndex); + } + + @Override + protected boolean onTouch(MotionEvent event) { + if (mUIListener != null) mUIListener.onUserInteraction(); + mGestureDetector.onTouchEvent(event); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mDownInScrolling = !mScroller.isFinished(); + mScroller.forceFinished(); + break; + } + return true; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public void setUserInteractionListener(UserInteractionListener listener) { + mUIListener = listener; + } + + public void setOverscrollEffect(int kind) { + mOverscrollEffect = kind; + mScroller.setOverfling(kind == OVERSCROLL_SYSTEM); + } + + @Override + protected void render(GLCanvas canvas) { + canvas.save(GLCanvas.SAVE_FLAG_CLIP); + canvas.clipRect(0, 0, getWidth(), getHeight()); + super.render(canvas); + + long currentTimeMillis = canvas.currentAnimationTimeMillis(); + boolean more = mScroller.advanceAnimation(currentTimeMillis); + boolean paperActive = (mOverscrollEffect == OVERSCROLL_3D) + && mPaper.advanceAnimation(currentTimeMillis); + updateScrollPosition(mScroller.getPosition(), false); + float interpolate = 1f; + if (mAnimation != null) { + more |= mAnimation.calculate(currentTimeMillis); + interpolate = mAnimation.value; + } + + more |= paperActive; + + if (WIDE) { + canvas.translate(-mScrollX, 0, 0); + } else { + canvas.translate(0, -mScrollY, 0); + } + + LinkedNode.List list = mItemList; + for (ItemEntry entry = list.getLast(); entry != null;) { + if (renderItem(canvas, entry, interpolate, 0, paperActive)) { + mCurrentItems.add(entry); + } + entry = list.previousOf(entry); + } + + int pass = 1; + while (!mCurrentItems.isEmpty()) { + for (int i = 0, n = mCurrentItems.size(); i < n; i++) { + ItemEntry entry = mCurrentItems.get(i); + if (renderItem(canvas, entry, interpolate, pass, paperActive)) { + mNextItems.add(entry); + } + } + mCurrentItems.clear(); + // swap mNextItems with mCurrentItems + ArrayList tmp = mNextItems; + mNextItems = mCurrentItems; + mCurrentItems = tmp; + pass += 1; + } + + if (WIDE) { + canvas.translate(mScrollX, 0, 0); + } else { + canvas.translate(0, mScrollY, 0); + } + + if (more) invalidate(); + if (mMoreAnimation && !more && mUIListener != null) { + mUIListener.onUserInteractionEnd(); + } + mMoreAnimation = more; + canvas.restore(); + } + + private boolean renderItem(GLCanvas canvas, ItemEntry entry, + float interpolate, int pass, boolean paperActive) { + canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX); + Position position = entry.target; + if (mPositions != null) { + position = mTempPosition; + position.set(entry.target); + position.x -= mScrollX; + position.y -= mScrollY; + Position source = mPositions + .getPosition(entry.item.getIdentity(), position); + source.x += mScrollX; + source.y += mScrollY; + position = mTempPosition; + Position.interpolate( + source, entry.target, position, interpolate); + } + canvas.multiplyAlpha(position.alpha); + if (paperActive) { + canvas.multiplyMatrix(mPaper.getTransform( + position, entry.base, mScrollX, mScrollY), 0); + } else { + canvas.translate(position.x, position.y, position.z); + } + canvas.rotate(position.theta, 0, 0, 1); + boolean more = entry.item.render(canvas, pass); + canvas.restore(); + return more; + } + + public static class MyAnimation extends Animation { + public float value; + + public MyAnimation() { + setInterpolator(new DecelerateInterpolator(4)); + setDuration(1500); + } + + @Override + protected void onCalculate(float progress) { + value = progress; + } + } + + private static class ItemEntry extends LinkedNode { + public DisplayItem item; + public Position target; + public Position base; + + public ItemEntry(DisplayItem item, Position target, Position base) { + this.item = item; + this.target = target; + this.base = base; + } + } + + public static class Layout { + + private int mVisibleStart; + private int mVisibleEnd; + + private int mSlotCount; + private int mSlotWidth; + private int mSlotHeight; + + private int mWidth; + private int mHeight; + + private int mUnitCount; + private int mContentLength; + private int mScrollPosition; + + private int mVerticalPadding; + private int mHorizontalPadding; + + public void setSlotSize(int slotWidth, int slotHeight) { + mSlotWidth = slotWidth; + mSlotHeight = slotHeight; + } + + public boolean setSlotCount(int slotCount) { + mSlotCount = slotCount; + int hPadding = mHorizontalPadding; + int vPadding = mVerticalPadding; + initLayoutParameters(); + return vPadding != mVerticalPadding || hPadding != mHorizontalPadding; + } + + public Rect getSlotRect(int index) { + int col, row; + if (WIDE) { + col = index / mUnitCount; + row = index - col * mUnitCount; + } else { + row = index / mUnitCount; + col = index - row * mUnitCount; + } + + int x = mHorizontalPadding + col * mSlotWidth; + int y = mVerticalPadding + row * mSlotHeight; + return new Rect(x, y, x + mSlotWidth, y + mSlotHeight); + } + + public int getContentLength() { + return mContentLength; + } + + // Calculate + // (1) mUnitCount: the number of slots we can fit into one column (or row). + // (2) mContentLength: the width (or height) we need to display all the + // columns (rows). + // (3) padding[]: the vertical and horizontal padding we need in order + // to put the slots towards to the center of the display. + // + // The "major" direction is the direction the user can scroll. The other + // direction is the "minor" direction. + // + // The comments inside this method are the description when the major + // directon is horizontal (X), and the minor directon is vertical (Y). + private void initLayoutParameters( + int majorLength, int minorLength, /* The view width and height */ + int majorUnitSize, int minorUnitSize, /* The slot width and height */ + int[] padding) { + int unitCount = minorLength / minorUnitSize; + if (unitCount == 0) unitCount = 1; + mUnitCount = unitCount; + + // We put extra padding above and below the column. + int availableUnits = Math.min(mUnitCount, mSlotCount); + padding[0] = (minorLength - availableUnits * minorUnitSize) / 2; + + // Then calculate how many columns we need for all slots. + int count = ((mSlotCount + mUnitCount - 1) / mUnitCount); + mContentLength = count * majorUnitSize; + + // If the content length is less then the screen width, put + // extra padding in left and right. + padding[1] = Math.max(0, (majorLength - mContentLength) / 2); + } + + private void initLayoutParameters() { + int[] padding = new int[2]; + if (WIDE) { + initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding); + mVerticalPadding = padding[0]; + mHorizontalPadding = padding[1]; + } else { + initLayoutParameters(mHeight, mWidth, mSlotHeight, mSlotWidth, padding); + mVerticalPadding = padding[1]; + mHorizontalPadding = padding[0]; + } + updateVisibleSlotRange(); + } + + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + initLayoutParameters(); + } + + private void updateVisibleSlotRange() { + int position = mScrollPosition; + + if (WIDE) { + int start = Math.max(0, (position / mSlotWidth) * mUnitCount); + int end = Math.min(mSlotCount, mUnitCount + * (position + mWidth + mSlotWidth - 1) / mSlotWidth); + setVisibleRange(start, end); + } else { + int start = Math.max(0, mUnitCount * (position / mSlotHeight)); + int end = Math.min(mSlotCount, mUnitCount + * (position + mHeight + mSlotHeight - 1) / mSlotHeight); + setVisibleRange(start, end); + } + } + + public void setScrollPosition(int position) { + if (mScrollPosition == position) return; + mScrollPosition = position; + updateVisibleSlotRange(); + } + + private void setVisibleRange(int start, int end) { + if (start == mVisibleStart && end == mVisibleEnd) return; + if (start < end) { + mVisibleStart = start; + mVisibleEnd = end; + } else { + mVisibleStart = mVisibleEnd = 0; + } + } + + public int getVisibleStart() { + return mVisibleStart; + } + + public int getVisibleEnd() { + return mVisibleEnd; + } + + public int getSlotIndexByPosition(float x, float y) { + float absoluteX = x + (WIDE ? mScrollPosition : 0); + absoluteX -= mHorizontalPadding; + int columnIdx = (int) (absoluteX + 0.5) / mSlotWidth; + if ((absoluteX - mSlotWidth * columnIdx) < 0 + || (!WIDE && columnIdx >= mUnitCount)) { + return INDEX_NONE; + } + + float absoluteY = y + (WIDE ? 0 : mScrollPosition); + absoluteY -= mVerticalPadding; + int rowIdx = (int) (absoluteY + 0.5) / mSlotHeight; + if (((absoluteY - mSlotHeight * rowIdx) < 0) + || (WIDE && rowIdx >= mUnitCount)) { + return INDEX_NONE; + } + int index = WIDE + ? (columnIdx * mUnitCount + rowIdx) + : (rowIdx * mUnitCount + columnIdx); + + return index >= mSlotCount ? INDEX_NONE : index; + } + + public int getScrollLimit() { + int limit = WIDE ? mContentLength - mWidth : mContentLength - mHeight; + return limit <= 0 ? 0 : limit; + } + } + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + + @Override + public boolean onFling(MotionEvent e1, + MotionEvent e2, float velocityX, float velocityY) { + int scrollLimit = mLayout.getScrollLimit(); + if (scrollLimit == 0) return false; + float velocity = WIDE ? velocityX : velocityY; + mScroller.fling((int) -velocity, 0, scrollLimit); + if (mUIListener != null) mUIListener.onUserInteractionBegin(); + invalidate(); + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, + MotionEvent e2, float distanceX, float distanceY) { + float distance = WIDE ? distanceX : distanceY; + boolean canMove = mScroller.startScroll( + Math.round(distance), 0, mLayout.getScrollLimit()); + if (mOverscrollEffect == OVERSCROLL_3D && !canMove) { + mPaper.overScroll(distance); + } + invalidate(); + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (mDownInScrolling) return true; + int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY()); + if (index != INDEX_NONE) mListener.onSingleTapUp(index); + return true; + } + + @Override + public void onLongPress(MotionEvent e) { + if (mDownInScrolling) return; + lockRendering(); + try { + int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY()); + if (index != INDEX_NONE) mListener.onLongTap(index); + } finally { + unlockRendering(); + } + } + } + + public void setStartIndex(int index) { + mStartIndex = index; + } + + // Return true if the layout parameters have been changed + public boolean setSlotCount(int slotCount) { + boolean changed = mLayout.setSlotCount(slotCount); + + // mStartIndex is applied the first time setSlotCount is called. + if (mStartIndex != INDEX_NONE) { + setCenterIndex(mStartIndex); + mStartIndex = INDEX_NONE; + } + updateScrollPosition(WIDE ? mScrollX : mScrollY, true); + return changed; + } + + public int getVisibleStart() { + return mLayout.getVisibleStart(); + } + + public int getVisibleEnd() { + return mLayout.getVisibleEnd(); + } + + public int getScrollX() { + return mScrollX; + } + + public int getScrollY() { + return mScrollY; + } +} diff --git a/src/com/android/gallery3d/ui/StaticBackground.java b/src/com/android/gallery3d/ui/StaticBackground.java new file mode 100644 index 000000000..08c55c378 --- /dev/null +++ b/src/com/android/gallery3d/ui/StaticBackground.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; + +public class StaticBackground extends GLView { + + private Context mContext; + private int mLandscapeResource; + private int mPortraitResource; + + private BasicTexture mBackground; + private boolean mIsLandscape = false; + + public StaticBackground(Context context) { + mContext = context; + } + + @Override + protected void onLayout(boolean changeSize, int l, int t, int r, int b) { + setOrientation(getWidth() >= getHeight()); + } + + private void setOrientation(boolean isLandscape) { + if (mIsLandscape == isLandscape) return; + mIsLandscape = isLandscape; + if (mBackground != null) mBackground.recycle(); + mBackground = new ResourceTexture( + mContext, mIsLandscape ? mLandscapeResource : mPortraitResource); + invalidate(); + } + + public void setImage(int landscapeId, int portraitId) { + mLandscapeResource = landscapeId; + mPortraitResource = portraitId; + if (mBackground != null) mBackground.recycle(); + mBackground = new ResourceTexture( + mContext, mIsLandscape ? landscapeId : portraitId); + invalidate(); + } + + @Override + protected void render(GLCanvas canvas) { + //mBackground.draw(canvas, 0, 0, getWidth(), getHeight()); + canvas.fillRect(0, 0, getWidth(), getHeight(), 0xFF000000); + } +} diff --git a/src/com/android/gallery3d/ui/StringTexture.java b/src/com/android/gallery3d/ui/StringTexture.java new file mode 100644 index 000000000..71ab9b351 --- /dev/null +++ b/src/com/android/gallery3d/ui/StringTexture.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint.FontMetricsInt; +import android.graphics.Typeface; +import android.text.TextPaint; +import android.text.TextUtils; + +// StringTexture is a texture shows the content of a specified String. +// +// To create a StringTexture, use the newInstance() method and specify +// the String, the font size, and the color. +class StringTexture extends CanvasTexture { + private final String mText; + private final TextPaint mPaint; + private final FontMetricsInt mMetrics; + + private StringTexture(String text, TextPaint paint, + FontMetricsInt metrics, int width, int height) { + super(width, height); + mText = text; + mPaint = paint; + mMetrics = metrics; + } + + public static TextPaint getDefaultPaint(float textSize, int color) { + TextPaint paint = new TextPaint(); + paint.setTextSize(textSize); + paint.setAntiAlias(true); + paint.setColor(color); + paint.setShadowLayer(2f, 0f, 0f, Color.BLACK); + return paint; + } + + public static StringTexture newInstance( + String text, float textSize, int color) { + return newInstance(text, getDefaultPaint(textSize, color)); + } + + public static StringTexture newInstance( + String text, String postfix, float textSize, int color, + float lengthLimit, boolean isBold) { + TextPaint paint = getDefaultPaint(textSize, color); + if (isBold) { + paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); + } + if (postfix != null) { + lengthLimit = Math.max(0, + lengthLimit - paint.measureText(postfix)); + text = TextUtils.ellipsize(text, paint, lengthLimit, + TextUtils.TruncateAt.END).toString() + postfix; + } else { + text = TextUtils.ellipsize( + text, paint, lengthLimit, TextUtils.TruncateAt.END).toString(); + } + return newInstance(text, paint); + } + + private static StringTexture newInstance(String text, TextPaint paint) { + FontMetricsInt metrics = paint.getFontMetricsInt(); + int width = (int) Math.ceil(paint.measureText(text)); + int height = metrics.bottom - metrics.top; + // The texture size needs to be at least 1x1. + if (width <= 0) width = 1; + if (height <= 0) height = 1; + return new StringTexture(text, paint, metrics, width, height); + } + + @Override + protected void onDraw(Canvas canvas, Bitmap backing) { + canvas.translate(0, -mMetrics.ascent); + canvas.drawText(mText, 0, 0, mPaint); + } +} diff --git a/src/com/android/gallery3d/ui/StripDrawer.java b/src/com/android/gallery3d/ui/StripDrawer.java new file mode 100644 index 000000000..09106128f --- /dev/null +++ b/src/com/android/gallery3d/ui/StripDrawer.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.Path; + +import android.content.Context; +import android.graphics.Rect; + +public class StripDrawer extends SelectionDrawer { + private NinePatchTexture mFocusBox; + private Rect mFocusBoxPadding; + + public StripDrawer(Context context) { + mFocusBox = new NinePatchTexture(context, R.drawable.focus_box); + mFocusBoxPadding = mFocusBox.getPaddings(); + } + + @Override + public void prepareDrawing() { + } + + @Override + public void draw(GLCanvas canvas, Texture content, int width, int height, + int rotation, Path path, int topIndex, int dataSourceType, + int mediaType, boolean wantCache, boolean isCaching) { + + int x = -width / 2; + int y = -height / 2; + + drawWithRotation(canvas, content, x, y, width, height, rotation); + } + + @Override + public void drawFocus(GLCanvas canvas, int width, int height) { + int x = -width / 2; + int y = -height / 2; + Rect p = mFocusBoxPadding; + mFocusBox.draw(canvas, x - p.left, y - p.top, + width + p.left + p.right, height + p.top + p.bottom); + } +} diff --git a/src/com/android/gallery3d/ui/SynchronizedHandler.java b/src/com/android/gallery3d/ui/SynchronizedHandler.java new file mode 100644 index 000000000..bd494a331 --- /dev/null +++ b/src/com/android/gallery3d/ui/SynchronizedHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.common.Utils; + +import android.os.Handler; +import android.os.Message; + +public class SynchronizedHandler extends Handler { + + private final GLRoot mRoot; + + public SynchronizedHandler(GLRoot root) { + mRoot = Utils.checkNotNull(root); + } + + @Override + public void dispatchMessage(Message message) { + mRoot.lockRenderThread(); + try { + super.dispatchMessage(message); + } finally { + mRoot.unlockRenderThread(); + } + } +} diff --git a/src/com/android/gallery3d/ui/TextButton.java b/src/com/android/gallery3d/ui/TextButton.java new file mode 100644 index 000000000..c6b85bf55 --- /dev/null +++ b/src/com/android/gallery3d/ui/TextButton.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import static com.android.gallery3d.ui.TextButtonConfig.*; + +import android.content.Context; +import android.graphics.Rect; +import android.view.MotionEvent; + +public class TextButton extends Label { + private static final String TAG = "TextButton"; + private boolean mPressed; + private Texture mPressedBackground; + private Texture mNormalBackground; + private OnClickedListener mOnClickListener; + + public interface OnClickedListener { + public void onClicked(GLView source); + } + + public TextButton(Context context, int label) { + super(context, label); + setPaddings(HORIZONTAL_PADDINGS, VERTICAL_PADDINGS, + HORIZONTAL_PADDINGS, VERTICAL_PADDINGS); + } + + public void setOnClickListener(OnClickedListener listener) { + mOnClickListener = listener; + } + + public void setPressedBackground(Texture texture) { + mPressedBackground = texture; + } + + public void setNormalBackground(Texture texture) { + mNormalBackground = texture; + } + + @SuppressWarnings("fallthrough") + @Override + protected boolean onTouch(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mPressed = true; + invalidate(); + break; + case MotionEvent.ACTION_UP: + if (mOnClickListener != null) { + mOnClickListener.onClicked(this); + } + // fall-through + case MotionEvent.ACTION_CANCEL: + mPressed = false; + invalidate(); + break; + } + return true; + } + + @Override + protected void render(GLCanvas canvas) { + Texture bg = mPressed ? mPressedBackground : mNormalBackground; + if (bg != null) { + int width = getWidth(); + int height = getHeight(); + if (bg instanceof NinePatchTexture) { + Rect p = ((NinePatchTexture) bg).getPaddings(); + bg.draw(canvas, -p.left, -p.top, + width + p.left + p.right, height + p.top + p.bottom); + } else { + bg.draw(canvas, 0, 0, width, height); + } + } + super.render(canvas); + } +} diff --git a/src/com/android/gallery3d/ui/Texture.java b/src/com/android/gallery3d/ui/Texture.java new file mode 100644 index 000000000..feb7b0ab7 --- /dev/null +++ b/src/com/android/gallery3d/ui/Texture.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +// Texture is a rectangular image which can be drawn on GLCanvas. +// The isOpaque() function gives a hint about whether the texture is opaque, +// so the drawing can be done faster. +// +// This is the current texture hierarchy: +// +// Texture +// -- ColorTexture +// -- BasicTexture +// -- RawTexture +// -- UploadedTexture +// -- BitmapTexture +// -- Tile +// -- ResourceTexture +// -- NinePatchTexture +// -- CanvasTexture +// -- DrawableTexture +// -- StringTexture +// +public interface Texture { + public int getWidth(); + public int getHeight(); + public void draw(GLCanvas canvas, int x, int y); + public void draw(GLCanvas canvas, int x, int y, int w, int h); + public boolean isOpaque(); +} diff --git a/src/com/android/gallery3d/ui/TileImageView.java b/src/com/android/gallery3d/ui/TileImageView.java new file mode 100644 index 000000000..cf0685191 --- /dev/null +++ b/src/com/android/gallery3d/ui/TileImageView.java @@ -0,0 +1,693 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.app.GalleryContext; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.RectF; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +public class TileImageView extends GLView { + public static final int SIZE_UNKNOWN = -1; + + @SuppressWarnings("unused") + private static final String TAG = "TileImageView"; + + // TILE_SIZE must be 2^N - 2. We put one pixel border in each side of the + // texture to avoid seams between tiles. + private static final int TILE_SIZE = 254; + private static final int TILE_BORDER = 1; + private static final int UPLOAD_LIMIT = 1; + + /* + * This is the tile state in the CPU side. + * Life of a Tile: + * ACTIVATED (initial state) + * --> IN_QUEUE - by queueForDecode() + * --> RECYCLED - by recycleTile() + * IN_QUEUE --> DECODING - by decodeTile() + * --> RECYCLED - by recycleTile) + * DECODING --> RECYCLING - by recycleTile() + * --> DECODED - by decodeTile() + * RECYCLING --> RECYCLED - by decodeTile() + * DECODED --> ACTIVATED - (after the decoded bitmap is uploaded) + * DECODED --> RECYCLED - by recycleTile() + * RECYCLED --> ACTIVATED - by obtainTile() + */ + private static final int STATE_ACTIVATED = 0x01; + private static final int STATE_IN_QUEUE = 0x02; + private static final int STATE_DECODING = 0x04; + private static final int STATE_DECODED = 0x08; + private static final int STATE_RECYCLING = 0x10; + private static final int STATE_RECYCLED = 0x20; + + private Model mModel; + protected BitmapTexture mBackupImage; + protected int mLevelCount; // cache the value of mScaledBitmaps.length + + // The mLevel variable indicates which level of bitmap we should use. + // Level 0 means the original full-sized bitmap, and a larger value means + // a smaller scaled bitmap (The width and height of each scaled bitmap is + // half size of the previous one). If the value is in [0, mLevelCount), we + // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value + // is mLevelCount, and that means we use mBackupTexture for display. + private int mLevel = 0; + + // The offsets of the (left, top) of the upper-left tile to the (left, top) + // of the view. + private int mOffsetX; + private int mOffsetY; + + private int mUploadQuota; + private boolean mRenderComplete; + + private final RectF mSourceRect = new RectF(); + private final RectF mTargetRect = new RectF(); + + private final HashMap mActiveTiles = new HashMap(); + + // The following three queue is guarded by TileImageView.this + private TileQueue mRecycledQueue = new TileQueue(); + private TileQueue mUploadQueue = new TileQueue(); + private TileQueue mDecodeQueue = new TileQueue(); + + // The width and height of the full-sized bitmap + protected int mImageWidth = SIZE_UNKNOWN; + protected int mImageHeight = SIZE_UNKNOWN; + + protected int mCenterX; + protected int mCenterY; + protected float mScale; + protected int mRotation; + + // Temp variables to avoid memory allocation + private final Rect mTileRange = new Rect(); + private final Rect mActiveRange[] = {new Rect(), new Rect()}; + + private final TileUploader mTileUploader = new TileUploader(); + private boolean mIsTextureFreed; + private Future mTileDecoder; + private ThreadPool mThreadPool; + private boolean mBackgroundTileUploaded; + + public static interface Model { + public int getLevelCount(); + public Bitmap getBackupImage(); + public int getImageWidth(); + public int getImageHeight(); + + // The method would be called in another thread + public Bitmap getTile(int level, int x, int y, int tileSize); + public boolean isFailedToLoad(); + } + + public TileImageView(GalleryContext context) { + mThreadPool = context.getThreadPool(); + mTileDecoder = mThreadPool.submit(new TileDecoder()); + } + + public void setModel(Model model) { + mModel = model; + if (model != null) notifyModelInvalidated(); + } + + private void updateBackupTexture(Bitmap backup) { + if (backup == null) { + if (mBackupImage != null) mBackupImage.recycle(); + mBackupImage = null; + } else { + if (mBackupImage != null) { + if (mBackupImage.getBitmap() != backup) { + mBackupImage.recycle(); + mBackupImage = new BitmapTexture(backup); + } + } else { + mBackupImage = new BitmapTexture(backup); + } + } + } + + public void notifyModelInvalidated() { + invalidateTiles(); + if (mModel == null) { + mBackupImage = null; + mImageWidth = 0; + mImageHeight = 0; + mLevelCount = 0; + } else { + updateBackupTexture(mModel.getBackupImage()); + mImageWidth = mModel.getImageWidth(); + mImageHeight = mModel.getImageHeight(); + mLevelCount = mModel.getLevelCount(); + } + layoutTiles(mCenterX, mCenterY, mScale, mRotation); + invalidate(); + } + + @Override + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + super.onLayout(changeSize, left, top, right, bottom); + if (changeSize) layoutTiles(mCenterX, mCenterY, mScale, mRotation); + } + + // Prepare the tiles we want to use for display. + // + // 1. Decide the tile level we want to use for display. + // 2. Decide the tile levels we want to keep as texture (in addition to + // the one we use for display). + // 3. Recycle unused tiles. + // 4. Activate the tiles we want. + private void layoutTiles(int centerX, int centerY, float scale, int rotation) { + // The width and height of this view. + int width = getWidth(); + int height = getHeight(); + + // The tile levels we want to keep as texture is in the range + // [fromLevel, endLevel). + int fromLevel; + int endLevel; + + // We want to use a texture larger than or equal to the display size. + mLevel = Utils.clamp(Utils.floorLog2(1f / scale), 0, mLevelCount); + + // We want to keep one more tile level as texture in addition to what + // we use for display. So it can be faster when the scale moves to the + // next level. We choose a level closer to the current scale. + if (mLevel != mLevelCount) { + Rect range = mTileRange; + getRange(range, centerX, centerY, mLevel, scale, rotation); + mOffsetX = Math.round(width / 2f + (range.left - centerX) * scale); + mOffsetY = Math.round(height / 2f + (range.top - centerY) * scale); + fromLevel = scale * (1 << mLevel) > 0.75f ? mLevel - 1 : mLevel; + } else { + // Activate the tiles of the smallest two levels. + fromLevel = mLevel - 2; + mOffsetX = Math.round(width / 2f - centerX * scale); + mOffsetY = Math.round(height / 2f - centerY * scale); + } + + fromLevel = Math.max(0, Math.min(fromLevel, mLevelCount - 2)); + endLevel = Math.min(fromLevel + 2, mLevelCount); + + Rect range[] = mActiveRange; + for (int i = fromLevel; i < endLevel; ++i) { + getRange(range[i - fromLevel], centerX, centerY, i, rotation); + } + + // If rotation is transient, don't update the tile. + if (rotation % 90 != 0) return; + + synchronized (this) { + mDecodeQueue.clean(); + mUploadQueue.clean(); + mBackgroundTileUploaded = false; + } + + // Recycle unused tiles: if the level of the active tile is outside the + // range [fromLevel, endLevel) or not in the visible range. + Iterator> + iter = mActiveTiles.entrySet().iterator(); + while (iter.hasNext()) { + Tile tile = iter.next().getValue(); + int level = tile.mTileLevel; + if (level < fromLevel || level >= endLevel + || !range[level - fromLevel].contains(tile.mX, tile.mY)) { + iter.remove(); + recycleTile(tile); + } + } + + for (int i = fromLevel; i < endLevel; ++i) { + int size = TILE_SIZE << i; + Rect r = range[i - fromLevel]; + for (int y = r.top, bottom = r.bottom; y < bottom; y += size) { + for (int x = r.left, right = r.right; x < right; x += size) { + activateTile(x, y, i); + } + } + } + invalidate(); + } + + protected synchronized void invalidateTiles() { + mDecodeQueue.clean(); + mUploadQueue.clean(); + // TODO disable decoder + for (Tile tile : mActiveTiles.values()) { + recycleTile(tile); + } + mActiveTiles.clear(); + } + + private void getRange(Rect out, int cX, int cY, int level, int rotation) { + getRange(out, cX, cY, level, 1f / (1 << (level + 1)), rotation); + } + + // If the bitmap is scaled by the given factor "scale", return the + // rectangle containing visible range. The left-top coordinate returned is + // aligned to the tile boundary. + // + // (cX, cY) is the point on the original bitmap which will be put in the + // center of the ImageViewer. + private void getRange(Rect out, + int cX, int cY, int level, float scale, int rotation) { + + double radians = Math.toRadians(-rotation); + double w = getWidth(); + double h = getHeight(); + + double cos = Math.cos(radians); + double sin = Math.sin(radians); + int width = (int) Math.ceil(Math.max( + Math.abs(cos * w - sin * h), Math.abs(cos * w + sin * h))); + int height = (int) Math.ceil(Math.max( + Math.abs(sin * w + cos * h), Math.abs(sin * w - cos * h))); + + int left = (int) Math.floor(cX - width / (2f * scale)); + int top = (int) Math.floor(cY - height / (2f * scale)); + int right = (int) Math.ceil(left + width / scale); + int bottom = (int) Math.ceil(top + height / scale); + + // align the rectangle to tile boundary + int size = TILE_SIZE << level; + left = Math.max(0, size * (left / size)); + top = Math.max(0, size * (top / size)); + right = Math.min(mImageWidth, right); + bottom = Math.min(mImageHeight, bottom); + + out.set(left, top, right, bottom); + } + + public boolean setPosition(int centerX, int centerY, float scale, int rotation) { + if (mCenterX == centerX + && mCenterY == centerY && mScale == scale) return false; + mCenterX = centerX; + mCenterY = centerY; + mScale = scale; + mRotation = rotation; + layoutTiles(centerX, centerY, scale, rotation); + invalidate(); + return true; + } + + public void freeTextures() { + mIsTextureFreed = true; + + if (mTileDecoder != null) { + mTileDecoder.cancel(); + mTileDecoder.get(); + mTileDecoder = null; + } + + for (Tile texture : mActiveTiles.values()) { + texture.recycle(); + } + mTileRange.set(0, 0, 0, 0); + mActiveTiles.clear(); + + synchronized (this) { + mUploadQueue.clean(); + mDecodeQueue.clean(); + Tile tile = mRecycledQueue.pop(); + while (tile != null) { + tile.recycle(); + tile = mRecycledQueue.pop(); + } + } + updateBackupTexture(null); + } + + public void prepareTextures() { + if (mTileDecoder == null) { + mTileDecoder = mThreadPool.submit(new TileDecoder()); + } + if (mIsTextureFreed) { + layoutTiles(mCenterX, mCenterY, mScale, mRotation); + mIsTextureFreed = false; + updateBackupTexture(mModel.getBackupImage()); + } + } + + @Override + protected void render(GLCanvas canvas) { + mUploadQuota = UPLOAD_LIMIT; + mRenderComplete = true; + + int level = mLevel; + int rotation = mRotation; + + if (rotation != 0) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + int centerX = getWidth() / 2, centerY = getHeight() / 2; + canvas.translate(centerX, centerY, 0); + canvas.rotate(rotation, 0, 0, 1); + canvas.translate(-centerX, -centerY, 0); + } + try { + if (level != mLevelCount) { + int size = (TILE_SIZE << level); + float length = size * mScale; + Rect r = mTileRange; + + for (int ty = r.top, i = 0; ty < r.bottom; ty += size, i++) { + float y = mOffsetY + i * length; + for (int tx = r.left, j = 0; tx < r.right; tx += size, j++) { + float x = mOffsetX + j * length; + drawTile(canvas, tx, ty, level, x, y, length); + } + } + } else if (mBackupImage != null) { + mBackupImage.draw(canvas, mOffsetX, mOffsetY, + Math.round(mImageWidth * mScale), + Math.round(mImageHeight * mScale)); + } + } finally { + if (rotation != 0) canvas.restore(); + } + + if (mRenderComplete) { + if (!mBackgroundTileUploaded) uploadBackgroundTiles(canvas); + } else { + invalidate(); + } + } + + private void uploadBackgroundTiles(GLCanvas canvas) { + mBackgroundTileUploaded = true; + for (Tile tile : mActiveTiles.values()) { + if (!tile.isContentValid(canvas)) queueForDecode(tile); + } + } + + void queueForUpload(Tile tile) { + synchronized (this) { + mUploadQueue.push(tile); + } + if (mTileUploader.mActive.compareAndSet(false, true)) { + getGLRoot().addOnGLIdleListener(mTileUploader); + } + } + + synchronized void queueForDecode(Tile tile) { + if (tile.mTileState == STATE_ACTIVATED) { + tile.mTileState = STATE_IN_QUEUE; + if (mDecodeQueue.push(tile)) notifyAll(); + } + } + + boolean decodeTile(Tile tile) { + synchronized (this) { + if (tile.mTileState != STATE_IN_QUEUE) return false; + tile.mTileState = STATE_DECODING; + } + boolean decodeComplete = tile.decode(); + synchronized (this) { + if (tile.mTileState == STATE_RECYCLING) { + tile.mTileState = STATE_RECYCLED; + tile.mDecodedTile = null; + mRecycledQueue.push(tile); + return false; + } + tile.mTileState = STATE_DECODED; + return decodeComplete; + } + } + + private synchronized Tile obtainTile(int x, int y, int level) { + Tile tile = mRecycledQueue.pop(); + if (tile != null) { + tile.mTileState = STATE_ACTIVATED; + tile.update(x, y, level); + return tile; + } + return new Tile(x, y, level); + } + + synchronized void recycleTile(Tile tile) { + if (tile.mTileState == STATE_DECODING) { + tile.mTileState = STATE_RECYCLING; + return; + } + tile.mTileState = STATE_RECYCLED; + tile.mDecodedTile = null; + mRecycledQueue.push(tile); + } + + private void activateTile(int x, int y, int level) { + Long key = makeTileKey(x, y, level); + Tile tile = mActiveTiles.get(key); + if (tile != null) { + if (tile.mTileState == STATE_IN_QUEUE) { + tile.mTileState = STATE_ACTIVATED; + } + return; + } + tile = obtainTile(x, y, level); + mActiveTiles.put(key, tile); + } + + private Tile getTile(int x, int y, int level) { + return mActiveTiles.get(makeTileKey(x, y, level)); + } + + private static Long makeTileKey(int x, int y, int level) { + long result = x; + result = (result << 16) | y; + result = (result << 16) | level; + return Long.valueOf(result); + } + + private class TileUploader implements GLRoot.OnGLIdleListener { + AtomicBoolean mActive = new AtomicBoolean(false); + + @Override + public boolean onGLIdle(GLRoot root, GLCanvas canvas) { + int quota = UPLOAD_LIMIT; + Tile tile; + while (true) { + synchronized (TileImageView.this) { + tile = mUploadQueue.pop(); + } + if (tile == null || quota <= 0) break; + if (!tile.isContentValid(canvas)) { + Utils.assertTrue(tile.mTileState == STATE_DECODED); + tile.updateContent(canvas); + --quota; + } + } + mActive.set(tile != null); + return tile != null; + } + } + + // Draw the tile to a square at canvas that locates at (x, y) and + // has a side length of length. + public void drawTile(GLCanvas canvas, + int tx, int ty, int level, float x, float y, float length) { + RectF source = mSourceRect; + RectF target = mTargetRect; + target.set(x, y, x + length, y + length); + source.set(0, 0, TILE_SIZE, TILE_SIZE); + + Tile tile = getTile(tx, ty, level); + if (tile != null) { + if (!tile.isContentValid(canvas)) { + if (tile.mTileState == STATE_DECODED) { + if (mUploadQuota > 0) { + --mUploadQuota; + tile.updateContent(canvas); + } else { + mRenderComplete = false; + } + } else { + mRenderComplete = false; + queueForDecode(tile); + } + } + if (drawTile(tile, canvas, source, target)) return; + } + if (mBackupImage != null) { + BasicTexture backup = mBackupImage; + int size = TILE_SIZE << level; + float scaleX = (float) backup.getWidth() / mImageWidth; + float scaleY = (float) backup.getHeight() / mImageHeight; + source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX, + (ty + size) * scaleY); + canvas.drawTexture(backup, source, target); + } + } + + // TODO: avoid drawing the unused part of the textures. + static boolean drawTile( + Tile tile, GLCanvas canvas, RectF source, RectF target) { + while (true) { + if (tile.isContentValid(canvas)) { + // offset source rectangle for the texture border. + source.offset(TILE_BORDER, TILE_BORDER); + canvas.drawTexture(tile, source, target); + return true; + } + + // Parent can be divided to four quads and tile is one of the four. + Tile parent = tile.getParentTile(); + if (parent == null) return false; + if (tile.mX == parent.mX) { + source.left /= 2f; + source.right /= 2f; + } else { + source.left = (TILE_SIZE + source.left) / 2f; + source.right = (TILE_SIZE + source.right) / 2f; + } + if (tile.mY == parent.mY) { + source.top /= 2f; + source.bottom /= 2f; + } else { + source.top = (TILE_SIZE + source.top) / 2f; + source.bottom = (TILE_SIZE + source.bottom) / 2f; + } + tile = parent; + } + } + + private class Tile extends UploadedTexture { + int mX; + int mY; + int mTileLevel; + Tile mNext; + Bitmap mDecodedTile; + volatile int mTileState = STATE_ACTIVATED; + + public Tile(int x, int y, int level) { + mX = x; + mY = y; + mTileLevel = level; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + bitmap.recycle(); + } + + boolean decode() { + // Get a tile from the original image. The tile is down-scaled + // by (1 << mTilelevel) from a region in the original image. + int tileLength = (TILE_SIZE + 2 * TILE_BORDER); + int borderLength = TILE_BORDER << mTileLevel; + try { + mDecodedTile = mModel.getTile( + mTileLevel, mX - borderLength, mY - borderLength, tileLength); + return mDecodedTile != null; + } catch (Throwable t) { + Log.w(TAG, "fail to decode tile", t); + return false; + } + } + + @Override + protected Bitmap onGetBitmap() { + Utils.assertTrue(mTileState == STATE_DECODED); + Bitmap bitmap = mDecodedTile; + mDecodedTile = null; + mTileState = STATE_ACTIVATED; + return bitmap; + } + + public void update(int x, int y, int level) { + mX = x; + mY = y; + mTileLevel = level; + invalidateContent(); + } + + public Tile getParentTile() { + if (mTileLevel + 1 == mLevelCount) return null; + int size = TILE_SIZE << (mTileLevel + 1); + int x = size * (mX / size); + int y = size * (mY / size); + return getTile(x, y, mTileLevel + 1); + } + + @Override + public String toString() { + return String.format("tile(%s, %s, %s / %s)", + mX / TILE_SIZE, mY / TILE_SIZE, mLevel, mLevelCount); + } + } + + private static class TileQueue { + private Tile mHead; + + public Tile pop() { + Tile tile = mHead; + if (tile != null) mHead = tile.mNext; + return tile; + } + + public boolean push(Tile tile) { + boolean wasEmpty = mHead == null; + tile.mNext = mHead; + mHead = tile; + return wasEmpty; + } + + public void clean() { + mHead = null; + } + } + + private class TileDecoder implements ThreadPool.Job { + + private CancelListener mNotifier = new CancelListener() { + @Override + public void onCancel() { + synchronized (TileImageView.this) { + TileImageView.this.notifyAll(); + } + } + }; + + @Override + public Void run(JobContext jc) { + jc.setMode(ThreadPool.MODE_NONE); + jc.setCancelListener(mNotifier); + while (!jc.isCancelled()) { + Tile tile = null; + synchronized(TileImageView.this) { + tile = mDecodeQueue.pop(); + if (tile == null && !jc.isCancelled()) { + Utils.waitWithoutInterrupt(TileImageView.this); + } + } + if (tile == null) continue; + if (decodeTile(tile)) queueForUpload(tile); + } + return null; + } + } +} diff --git a/src/com/android/gallery3d/ui/TileImageViewAdapter.java b/src/com/android/gallery3d/ui/TileImageViewAdapter.java new file mode 100644 index 000000000..65dea0eac --- /dev/null +++ b/src/com/android/gallery3d/ui/TileImageViewAdapter.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.common.Utils; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Canvas; +import android.graphics.Rect; + +public class TileImageViewAdapter implements TileImageView.Model { + protected BitmapRegionDecoder mRegionDecoder; + protected int mImageWidth; + protected int mImageHeight; + protected Bitmap mBackupImage; + protected int mLevelCount; + protected boolean mFailedToLoad; + + private final Rect mIntersectRect = new Rect(); + private final Rect mRegionRect = new Rect(); + + public TileImageViewAdapter() { + } + + public TileImageViewAdapter(Bitmap backup, BitmapRegionDecoder regionDecoder) { + mBackupImage = Utils.checkNotNull(backup); + mRegionDecoder = regionDecoder; + mImageWidth = regionDecoder.getWidth(); + mImageHeight = regionDecoder.getHeight(); + mLevelCount = calculateLevelCount(); + } + + public synchronized void clear() { + mBackupImage = null; + mImageWidth = 0; + mImageHeight = 0; + mLevelCount = 0; + mRegionDecoder = null; + mFailedToLoad = false; + } + + public synchronized void setBackupImage(Bitmap backup, int width, int height) { + mBackupImage = Utils.checkNotNull(backup); + mImageWidth = width; + mImageHeight = height; + mRegionDecoder = null; + mLevelCount = 0; + mFailedToLoad = false; + } + + public synchronized void setRegionDecoder(BitmapRegionDecoder decoder) { + mRegionDecoder = Utils.checkNotNull(decoder); + mImageWidth = decoder.getWidth(); + mImageHeight = decoder.getHeight(); + mLevelCount = calculateLevelCount(); + mFailedToLoad = false; + } + + private int calculateLevelCount() { + return Math.max(0, Utils.ceilLog2( + (float) mImageWidth / mBackupImage.getWidth())); + } + + @Override + public synchronized Bitmap getTile(int level, int x, int y, int length) { + Rect region = mRegionRect; + Rect intersectRect = mIntersectRect; + region.set(x, y, x + (length << level), y + (length << level)); + intersectRect.set(0, 0, mImageWidth, mImageHeight); + + // Get the intersected rect of the requested region and the image. + Utils.assertTrue(intersectRect.intersect(region)); + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Config.ARGB_8888; + options.inPreferQualityOverSpeed = true; + options.inSampleSize = (1 << level); + + Bitmap bitmap; + + // In CropImage, we may call the decodeRegion() concurrently. + synchronized (mRegionDecoder) { + bitmap = mRegionDecoder.decodeRegion(intersectRect, options); + } + + // The returned region may not match with the targetLength. + // If so, we fill black pixels on it. + if (intersectRect.equals(region)) return bitmap; + + Bitmap tile = Bitmap.createBitmap(length, length, Config.ARGB_8888); + Canvas canvas = new Canvas(tile); + canvas.drawBitmap(bitmap, + (intersectRect.left - region.left) >> level, + (intersectRect.top - region.top) >> level, null); + bitmap.recycle(); + return tile; + } + + @Override + public Bitmap getBackupImage() { + return mBackupImage; + } + + @Override + public int getImageHeight() { + return mImageHeight; + } + + @Override + public int getImageWidth() { + return mImageWidth; + } + + @Override + public int getLevelCount() { + return mLevelCount; + } + + public void setFailedToLoad() { + mFailedToLoad = true; + } + + @Override + public boolean isFailedToLoad() { + return mFailedToLoad; + } +} diff --git a/src/com/android/gallery3d/ui/UploadedTexture.java b/src/com/android/gallery3d/ui/UploadedTexture.java new file mode 100644 index 000000000..b063824d2 --- /dev/null +++ b/src/com/android/gallery3d/ui/UploadedTexture.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.common.Utils; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.opengl.GLUtils; + +import java.util.HashMap; +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11Ext; + +// UploadedTextures use a Bitmap for the content of the texture. +// +// Subclasses should implement onGetBitmap() to provide the Bitmap and +// implement onFreeBitmap(mBitmap) which will be called when the Bitmap +// is not needed anymore. +// +// isContentValid() is meaningful only when the isLoaded() returns true. +// It means whether the content needs to be updated. +// +// The user of this class should call recycle() when the texture is not +// needed anymore. +// +// By default an UploadedTexture is opaque (so it can be drawn faster without +// blending). The user or subclass can override it using setOpaque(). +abstract class UploadedTexture extends BasicTexture { + + // To prevent keeping allocation the borders, we store those used borders here. + // Since the length will be power of two, it won't use too much memory. + private static HashMap sBorderLines = + new HashMap(); + private static BorderKey sBorderKey = new BorderKey(); + + @SuppressWarnings("unused") + private static final String TAG = "Texture"; + private boolean mContentValid = true; + private boolean mOpaque = true; + private boolean mThrottled = false; + private static int sUploadedCount; + private static final int UPLOAD_LIMIT = 100; + + protected Bitmap mBitmap; + + protected UploadedTexture() { + super(null, 0, STATE_UNLOADED); + } + + private static class BorderKey implements Cloneable { + public boolean vertical; + public Config config; + public int length; + + @Override + public int hashCode() { + int x = config.hashCode() ^ length; + return vertical ? x : -x; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof BorderKey)) return false; + BorderKey o = (BorderKey) object; + return vertical == o.vertical + && config == o.config && length == o.length; + } + + @Override + public BorderKey clone() { + try { + return (BorderKey) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(e); + } + } + } + + protected void setThrottled(boolean throttled) { + mThrottled = throttled; + } + + private static Bitmap getBorderLine( + boolean vertical, Config config, int length) { + BorderKey key = sBorderKey; + key.vertical = vertical; + key.config = config; + key.length = length; + Bitmap bitmap = sBorderLines.get(key); + if (bitmap == null) { + bitmap = vertical + ? Bitmap.createBitmap(1, length, config) + : Bitmap.createBitmap(length, 1, config); + sBorderLines.put(key.clone(), bitmap); + } + return bitmap; + } + + private Bitmap getBitmap() { + if (mBitmap == null) { + mBitmap = onGetBitmap(); + if (mWidth == UNSPECIFIED) { + setSize(mBitmap.getWidth(), mBitmap.getHeight()); + } else if (mWidth != mBitmap.getWidth() + || mHeight != mBitmap.getHeight()) { + throw new IllegalStateException(String.format( + "cannot change size: this = %s, orig = %sx%s, new = %sx%s", + toString(), mWidth, mHeight, mBitmap.getWidth(), + mBitmap.getHeight())); + } + } + return mBitmap; + } + + private void freeBitmap() { + Utils.assertTrue(mBitmap != null); + onFreeBitmap(mBitmap); + mBitmap = null; + } + + @Override + public int getWidth() { + if (mWidth == UNSPECIFIED) getBitmap(); + return mWidth; + } + + @Override + public int getHeight() { + if (mWidth == UNSPECIFIED) getBitmap(); + return mHeight; + } + + protected abstract Bitmap onGetBitmap(); + + protected abstract void onFreeBitmap(Bitmap bitmap); + + protected void invalidateContent() { + if (mBitmap != null) freeBitmap(); + mContentValid = false; + } + + /** + * Whether the content on GPU is valid. + */ + public boolean isContentValid(GLCanvas canvas) { + return isLoaded(canvas) && mContentValid; + } + + /** + * Updates the content on GPU's memory. + * @param canvas + */ + public void updateContent(GLCanvas canvas) { + if (!isLoaded(canvas)) { + if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) { + return; + } + uploadToCanvas(canvas); + } else if (!mContentValid) { + Bitmap bitmap = getBitmap(); + int format = GLUtils.getInternalFormat(bitmap); + int type = GLUtils.getType(bitmap); + canvas.getGLInstance().glBindTexture(GL11.GL_TEXTURE_2D, mId); + GLUtils.texSubImage2D( + GL11.GL_TEXTURE_2D, 0, 0, 0, bitmap, format, type); + freeBitmap(); + mContentValid = true; + } + } + + public static void resetUploadLimit() { + sUploadedCount = 0; + } + + public static boolean uploadLimitReached() { + return sUploadedCount > UPLOAD_LIMIT; + } + + static int[] sTextureId = new int[1]; + static float[] sCropRect = new float[4]; + + private void uploadToCanvas(GLCanvas canvas) { + GL11 gl = canvas.getGLInstance(); + + Bitmap bitmap = getBitmap(); + if (bitmap != null) { + try { + // Define a vertically flipped crop rectangle for + // OES_draw_texture. + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + sCropRect[0] = 0; + sCropRect[1] = height; + sCropRect[2] = width; + sCropRect[3] = -height; + + // Upload the bitmap to a new texture. + gl.glGenTextures(1, sTextureId, 0); + gl.glBindTexture(GL11.GL_TEXTURE_2D, sTextureId[0]); + gl.glTexParameterfv(GL11.GL_TEXTURE_2D, + GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0); + gl.glTexParameteri(GL11.GL_TEXTURE_2D, + GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL11.GL_TEXTURE_2D, + GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE); + gl.glTexParameterf(GL11.GL_TEXTURE_2D, + GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + gl.glTexParameterf(GL11.GL_TEXTURE_2D, + GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + + if (width == getTextureWidth() && height == getTextureHeight()) { + GLUtils.texImage2D(GL11.GL_TEXTURE_2D, 0, bitmap, 0); + } else { + int format = GLUtils.getInternalFormat(bitmap); + int type = GLUtils.getType(bitmap); + Config config = bitmap.getConfig(); + + gl.glTexImage2D(GL11.GL_TEXTURE_2D, 0, format, + getTextureWidth(), getTextureHeight(), + 0, format, type, null); + GLUtils.texSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, bitmap, + format, type); + + if (width != getTextureWidth()) { + Bitmap line = getBorderLine(true, config, getTextureHeight()); + GLUtils.texSubImage2D( + GL11.GL_TEXTURE_2D, 0, width, 0, line, format, type); + } + + if (height != getTextureHeight()) { + Bitmap line = getBorderLine(false, config, getTextureWidth()); + GLUtils.texSubImage2D( + GL11.GL_TEXTURE_2D, 0, 0, height, line, format, type); + } + + } + } finally { + freeBitmap(); + } + // Update texture state. + setAssociatedCanvas(canvas); + mId = sTextureId[0]; + mState = UploadedTexture.STATE_LOADED; + mContentValid = true; + } else { + mState = STATE_ERROR; + throw new RuntimeException("Texture load fail, no bitmap"); + } + } + + @Override + protected boolean onBind(GLCanvas canvas) { + updateContent(canvas); + return isContentValid(canvas); + } + + public void setOpaque(boolean isOpaque) { + mOpaque = isOpaque; + } + + public boolean isOpaque() { + return mOpaque; + } + + @Override + public void recycle() { + super.recycle(); + if (mBitmap != null) freeBitmap(); + } +} diff --git a/src/com/android/gallery3d/ui/UserInteractionListener.java b/src/com/android/gallery3d/ui/UserInteractionListener.java new file mode 100644 index 000000000..bc4a71800 --- /dev/null +++ b/src/com/android/gallery3d/ui/UserInteractionListener.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +public interface UserInteractionListener { + // Called when a user interaction begins (for example, fling). + public void onUserInteractionBegin(); + // Called when the user interaction ends. + public void onUserInteractionEnd(); + // Other one-shot user interactions. + public void onUserInteraction(); +} diff --git a/src/com/android/gallery3d/util/CacheManager.java b/src/com/android/gallery3d/util/CacheManager.java new file mode 100644 index 000000000..fcc444e98 --- /dev/null +++ b/src/com/android/gallery3d/util/CacheManager.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +import com.android.gallery3d.common.BlobCache; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; + +public class CacheManager { + private static final String TAG = "CacheManager"; + private static final String KEY_CACHE_UP_TO_DATE = "cache-up-to-date"; + private static HashMap sCacheMap = + new HashMap(); + private static boolean sOldCheckDone = false; + + // Return null when we cannot instantiate a BlobCache, e.g.: + // there is no SD card found. + // This can only be called from data thread. + public static BlobCache getCache(Context context, String filename, + int maxEntries, int maxBytes, int version) { + synchronized (sCacheMap) { + if (!sOldCheckDone) { + removeOldFilesIfNecessary(context); + sOldCheckDone = true; + } + BlobCache cache = sCacheMap.get(filename); + if (cache == null) { + File cacheDir = context.getExternalCacheDir(); + String path = cacheDir.getAbsolutePath() + "/" + filename; + try { + cache = new BlobCache(path, maxEntries, maxBytes, false, + version); + sCacheMap.put(filename, cache); + } catch (IOException e) { + Log.e(TAG, "Cannot instantiate cache!", e); + } + } + return cache; + } + } + + // Removes the old files if the data is wiped. + private static void removeOldFilesIfNecessary(Context context) { + SharedPreferences pref = PreferenceManager + .getDefaultSharedPreferences(context); + int n = 0; + try { + n = pref.getInt(KEY_CACHE_UP_TO_DATE, 0); + } catch (Throwable t) { + // ignore. + } + if (n != 0) return; + pref.edit().putInt(KEY_CACHE_UP_TO_DATE, 1).commit(); + + File cacheDir = context.getExternalCacheDir(); + String prefix = cacheDir.getAbsolutePath() + "/"; + + BlobCache.deleteFiles(prefix + "imgcache"); + BlobCache.deleteFiles(prefix + "rev_geocoding"); + BlobCache.deleteFiles(prefix + "bookmark"); + } +} diff --git a/src/com/android/gallery3d/util/Future.java b/src/com/android/gallery3d/util/Future.java new file mode 100644 index 000000000..580a2a120 --- /dev/null +++ b/src/com/android/gallery3d/util/Future.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +// This Future differs from the java.util.concurrent.Future in these aspects: +// +// - Once cancel() is called, isCancelled() always returns true. It is a sticky +// flag used to communicate to the implementation. The implmentation may +// ignore that flag. Regardless whether the Future is cancelled, a return +// value will be provided to get(). The implementation may choose to return +// null if it finds the Future is cancelled. +// +// - get() does not throw exceptions. +// +public interface Future { + public void cancel(); + public boolean isCancelled(); + public boolean isDone(); + public T get(); + public void waitDone(); +} diff --git a/src/com/android/gallery3d/util/FutureListener.java b/src/com/android/gallery3d/util/FutureListener.java new file mode 100644 index 000000000..ed1f820c7 --- /dev/null +++ b/src/com/android/gallery3d/util/FutureListener.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +public interface FutureListener { + public void onFutureDone(Future future); +} diff --git a/src/com/android/gallery3d/util/FutureTask.java b/src/com/android/gallery3d/util/FutureTask.java new file mode 100644 index 000000000..9cfab27cb --- /dev/null +++ b/src/com/android/gallery3d/util/FutureTask.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +import java.util.concurrent.Callable; + +// NOTE: If the Callable throws any Throwable, the result value will be null. +public class FutureTask implements Runnable, Future { + private static final String TAG = "FutureTask"; + private Callable mCallable; + private FutureListener mListener; + private volatile boolean mIsCancelled; + private boolean mIsDone; + private T mResult; + + public FutureTask(Callable callable, FutureListener listener) { + mCallable = callable; + mListener = listener; + } + + public FutureTask(Callable callable) { + this(callable, null); + } + + public void cancel() { + mIsCancelled = true; + } + + public synchronized T get() { + while (!mIsDone) { + try { + wait(); + } catch (InterruptedException t) { + // ignore. + } + } + return mResult; + } + + public void waitDone() { + get(); + } + + public synchronized boolean isDone() { + return mIsDone; + } + + public boolean isCancelled() { + return mIsCancelled; + } + + public void run() { + T result = null; + + if (!mIsCancelled) { + try { + result = mCallable.call(); + } catch (Throwable ex) { + Log.w(TAG, "Exception in running a task", ex); + } + } + + synchronized(this) { + mResult = result; + mIsDone = true; + if (mListener != null) { + mListener.onFutureDone(this); + } + notifyAll(); + } + } +} diff --git a/src/com/android/gallery3d/util/GalleryUtils.java b/src/com/android/gallery3d/util/GalleryUtils.java new file mode 100644 index 000000000..2fed46a22 --- /dev/null +++ b/src/com/android/gallery3d/util/GalleryUtils.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.PackagesMonitor; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.ConditionVariable; +import android.os.Environment; +import android.os.StatFs; +import android.preference.PreferenceManager; +import android.provider.MediaStore; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.WindowManager; + +import java.util.Arrays; +import java.util.List; + +public class GalleryUtils { + private static final String TAG = "GalleryUtils"; + private static final String MAPS_PACKAGE_NAME = "com.google.android.apps.maps"; + private static final String MAPS_CLASS_NAME = "com.google.android.maps.MapsActivity"; + + private static final String MIME_TYPE_IMAGE = "image/*"; + private static final String MIME_TYPE_VIDEO = "video/*"; + private static final String MIME_TYPE_ALL = "*/*"; + + private static final String PREFIX_PHOTO_EDITOR_UPDATE = "editor-update-"; + private static final String PREFIX_HAS_PHOTO_EDITOR = "has-editor-"; + + private static final String KEY_CAMERA_UPDATE = "camera-update"; + private static final String KEY_HAS_CAMERA = "has-camera"; + + private static Context sContext; + + + static float sPixelDensity = -1f; + + public static void initialize(Context context) { + sContext = context; + if (sPixelDensity < 0) { + DisplayMetrics metrics = new DisplayMetrics(); + WindowManager wm = (WindowManager) + context.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(metrics); + sPixelDensity = metrics.density; + } + } + + public static float dpToPixel(float dp) { + return sPixelDensity * dp; + } + + public static int dpToPixel(int dp) { + return Math.round(dpToPixel((float) dp)); + } + + public static int meterToPixel(float meter) { + // 1 meter = 39.37 inches, 1 inch = 160 dp. + return Math.round(dpToPixel(meter * 39.37f * 160)); + } + + public static byte[] getBytes(String in) { + byte[] result = new byte[in.length() * 2]; + int output = 0; + for (char ch : in.toCharArray()) { + result[output++] = (byte) (ch & 0xFF); + result[output++] = (byte) (ch >> 8); + } + return result; + } + + // Below are used the detect using database in the render thread. It only + // works most of the time, but that's ok because it's for debugging only. + + private static volatile Thread sCurrentThread; + private static volatile boolean sWarned; + + public static void setRenderThread() { + sCurrentThread = Thread.currentThread(); + } + + public static void assertNotInRenderThread() { + if (!sWarned) { + if (Thread.currentThread() == sCurrentThread) { + sWarned = true; + Log.w(TAG, new Throwable("Should not do this in render thread")); + } + } + } + + private static final double RAD_PER_DEG = Math.PI / 180.0; + private static final double EARTH_RADIUS_METERS = 6367000.0; + + public static double fastDistanceMeters(double latRad1, double lngRad1, + double latRad2, double lngRad2) { + if ((Math.abs(latRad1 - latRad2) > RAD_PER_DEG) + || (Math.abs(lngRad1 - lngRad2) > RAD_PER_DEG)) { + return accurateDistanceMeters(latRad1, lngRad1, latRad2, lngRad2); + } + // Approximate sin(x) = x. + double sineLat = (latRad1 - latRad2); + + // Approximate sin(x) = x. + double sineLng = (lngRad1 - lngRad2); + + // Approximate cos(lat1) * cos(lat2) using + // cos((lat1 + lat2)/2) ^ 2 + double cosTerms = Math.cos((latRad1 + latRad2) / 2.0); + cosTerms = cosTerms * cosTerms; + double trigTerm = sineLat * sineLat + cosTerms * sineLng * sineLng; + trigTerm = Math.sqrt(trigTerm); + + // Approximate arcsin(x) = x + return EARTH_RADIUS_METERS * trigTerm; + } + + public static double accurateDistanceMeters(double lat1, double lng1, + double lat2, double lng2) { + double dlat = Math.sin(0.5 * (lat2 - lat1)); + double dlng = Math.sin(0.5 * (lng2 - lng1)); + double x = dlat * dlat + dlng * dlng * Math.cos(lat1) * Math.cos(lat2); + return (2 * Math.atan2(Math.sqrt(x), Math.sqrt(Math.max(0.0, + 1.0 - x)))) * EARTH_RADIUS_METERS; + } + + + public static final double toMile(double meter) { + return meter / 1609; + } + + // For debugging, it will block the caller for timeout millis. + public static void fakeBusy(JobContext jc, int timeout) { + final ConditionVariable cv = new ConditionVariable(); + jc.setCancelListener(new CancelListener() { + public void onCancel() { + cv.open(); + } + }); + cv.block(timeout); + jc.setCancelListener(null); + } + + public static boolean isEditorAvailable(Context context, String mimeType) { + int version = PackagesMonitor.getPackagesVersion(context); + + String updateKey = PREFIX_PHOTO_EDITOR_UPDATE + mimeType; + String hasKey = PREFIX_HAS_PHOTO_EDITOR + mimeType; + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + if (prefs.getInt(updateKey, 0) != version) { + PackageManager packageManager = context.getPackageManager(); + List infos = packageManager.queryIntentActivities( + new Intent(Intent.ACTION_EDIT).setType(mimeType), 0); + prefs.edit().putInt(updateKey, version) + .putBoolean(hasKey, !infos.isEmpty()) + .commit(); + } + + return prefs.getBoolean(hasKey, true); + } + + public static boolean isCameraAvailable(Context context) { + int version = PackagesMonitor.getPackagesVersion(context); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + if (prefs.getInt(KEY_CAMERA_UPDATE, 0) != version) { + PackageManager packageManager = context.getPackageManager(); + List infos = packageManager.queryIntentActivities( + new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA), 0); + prefs.edit().putInt(KEY_CAMERA_UPDATE, version) + .putBoolean(KEY_HAS_CAMERA, !infos.isEmpty()) + .commit(); + } + return prefs.getBoolean(KEY_HAS_CAMERA, true); + } + + public static boolean isValidLocation(double latitude, double longitude) { + // TODO: change || to && after we fix the default location issue + return (latitude != MediaItem.INVALID_LATLNG || longitude != MediaItem.INVALID_LATLNG); + } + public static void showOnMap(Context context, double latitude, double longitude) { + try { + // We don't use "geo:latitude,longitude" because it only centers + // the MapView to the specified location, but we need a marker + // for further operations (routing to/from). + // The q=(lat, lng) syntax is suggested by geo-team. + String uri = String.format("http://maps.google.com/maps?f=q&q=(%f,%f)", + latitude, longitude); + ComponentName compName = new ComponentName(MAPS_PACKAGE_NAME, + MAPS_CLASS_NAME); + Intent mapsIntent = new Intent(Intent.ACTION_VIEW, + Uri.parse(uri)).setComponent(compName); + context.startActivity(mapsIntent); + } catch (ActivityNotFoundException e) { + // Use the "geo intent" if no GMM is installed + Log.e(TAG, "GMM activity not found!", e); + String url = String.format("geo:%f,%f", latitude, longitude); + Intent mapsIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + context.startActivity(mapsIntent); + } + } + + public static void setViewPointMatrix( + float matrix[], float x, float y, float z) { + // The matrix is + // -z, 0, x, 0 + // 0, -z, y, 0 + // 0, 0, 1, 0 + // 0, 0, 1, -z + Arrays.fill(matrix, 0, 16, 0); + matrix[0] = matrix[5] = matrix[15] = -z; + matrix[8] = x; + matrix[9] = y; + matrix[10] = matrix[11] = 1; + } + + public static int getBucketId(String path) { + return path.toLowerCase().hashCode(); + } + + // Returns a (localized) string for the given duration (in seconds). + public static String formatDuration(final Context context, int duration) { + int h = duration / 3600; + int m = (duration - h * 3600) / 60; + int s = duration - (h * 3600 + m * 60); + String durationValue; + if (h == 0) { + durationValue = String.format(context.getString(R.string.details_ms), m, s); + } else { + durationValue = String.format(context.getString(R.string.details_hms), h, m, s); + } + return durationValue; + } + + public static void setSpinnerVisibility(final Activity activity, + final boolean visible) { + activity.runOnUiThread(new Runnable() { + public void run() { + activity.setProgressBarIndeterminateVisibility(visible); + } + }); + } + + public static int determineTypeBits(Context context, Intent intent) { + int typeBits = 0; + String type = intent.resolveType(context); + if (intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false)) { + if (MIME_TYPE_ALL.equals(type)) { + typeBits = DataManager.INCLUDE_LOCAL_ALL_ONLY; + } else if (MIME_TYPE_IMAGE.equals(type)) { + typeBits = DataManager.INCLUDE_LOCAL_IMAGE_ONLY; + } else if (MIME_TYPE_VIDEO.equals(type)) { + typeBits = DataManager.INCLUDE_LOCAL_VIDEO_ONLY; + } + } else { + if (MIME_TYPE_ALL.equals(type)) { + typeBits = DataManager.INCLUDE_ALL; + } else if (MIME_TYPE_IMAGE.equals(type)) { + typeBits = DataManager.INCLUDE_IMAGE; + } else if (MIME_TYPE_VIDEO.equals(type)) { + typeBits = DataManager.INCLUDE_VIDEO; + } + } + if (typeBits == 0) typeBits = DataManager.INCLUDE_ALL; + + return typeBits; + } + + public static int getSelectionModePrompt(int typeBits) { + if ((typeBits & DataManager.INCLUDE_VIDEO) != 0) { + return (typeBits & DataManager.INCLUDE_IMAGE) == 0 + ? R.string.select_video + : R.string.select_item; + } + return R.string.select_image; + } + + public static boolean hasSpaceForSize(long size) { + String state = Environment.getExternalStorageState(); + if (!Environment.MEDIA_MOUNTED.equals(state)) { + return false; + } + + String path = Environment.getExternalStorageDirectory().getPath(); + try { + StatFs stat = new StatFs(path); + return stat.getAvailableBlocks() * (long) stat.getBlockSize() > size; + } catch (Exception e) { + Log.i(TAG, "Fail to access external storage", e); + } + return false; + } + + public static void assertInMainThread() { + if (Thread.currentThread() == sContext.getMainLooper().getThread()) { + throw new AssertionError(); + } + } +} diff --git a/src/com/android/gallery3d/util/IdentityCache.java b/src/com/android/gallery3d/util/IdentityCache.java new file mode 100644 index 000000000..02a46aef7 --- /dev/null +++ b/src/com/android/gallery3d/util/IdentityCache.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.ArrayList; +import java.util.Set; + +public class IdentityCache { + + private final HashMap> mWeakMap = + new HashMap>(); + private ReferenceQueue mQueue = new ReferenceQueue(); + + public IdentityCache() { + } + + private static class Entry extends WeakReference { + K mKey; + + public Entry(K key, V value, ReferenceQueue queue) { + super(value, queue); + mKey = key; + } + } + + private void cleanUpWeakMap() { + Entry entry = (Entry) mQueue.poll(); + while (entry != null) { + mWeakMap.remove(entry.mKey); + entry = (Entry) mQueue.poll(); + } + } + + public synchronized V put(K key, V value) { + cleanUpWeakMap(); + Entry entry = mWeakMap.put( + key, new Entry(key, value, mQueue)); + return entry == null ? null : entry.get(); + } + + public synchronized V get(K key) { + cleanUpWeakMap(); + Entry entry = mWeakMap.get(key); + return entry == null ? null : entry.get(); + } + + public synchronized void clear() { + mWeakMap.clear(); + mQueue = new ReferenceQueue(); + } + + public synchronized ArrayList keys() { + Set set = mWeakMap.keySet(); + ArrayList result = new ArrayList(set); + return result; + } +} diff --git a/src/com/android/gallery3d/util/IntArray.java b/src/com/android/gallery3d/util/IntArray.java new file mode 100644 index 000000000..88657bbd6 --- /dev/null +++ b/src/com/android/gallery3d/util/IntArray.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +public class IntArray { + private static final int INIT_CAPACITY = 8; + + private int mData[] = new int[INIT_CAPACITY]; + private int mSize = 0; + + public void add(int value) { + if (mData.length == mSize) { + int temp[] = new int[mSize + mSize]; + System.arraycopy(mData, 0, temp, 0, mSize); + mData = temp; + } + mData[mSize++] = value; + } + + public int size() { + return mSize; + } + + public int[] toArray(int[] result) { + if (result == null || result.length < mSize) { + result = new int[mSize]; + } + System.arraycopy(mData, 0, result, 0, mSize); + return result; + } + + public int[] getInternalArray() { + return mData; + } + + public void clear() { + mSize = 0; + if (mData.length != INIT_CAPACITY) mData = new int[INIT_CAPACITY]; + } +} diff --git a/src/com/android/gallery3d/util/InterruptableOutputStream.java b/src/com/android/gallery3d/util/InterruptableOutputStream.java new file mode 100644 index 000000000..1ab62ab98 --- /dev/null +++ b/src/com/android/gallery3d/util/InterruptableOutputStream.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +import com.android.gallery3d.common.Utils; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.OutputStream; + +public class InterruptableOutputStream extends OutputStream { + + private static final int MAX_WRITE_BYTES = 4096; + + private OutputStream mOutputStream; + private volatile boolean mIsInterrupted = false; + + public InterruptableOutputStream(OutputStream outputStream) { + mOutputStream = Utils.checkNotNull(outputStream); + } + + @Override + public void write(int oneByte) throws IOException { + if (mIsInterrupted) throw new InterruptedIOException(); + mOutputStream.write(oneByte); + } + + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + int end = offset + count; + while (offset < end) { + if (mIsInterrupted) throw new InterruptedIOException(); + int bytesCount = Math.min(MAX_WRITE_BYTES, end - offset); + mOutputStream.write(buffer, offset, bytesCount); + offset += bytesCount; + } + } + + @Override + public void close() throws IOException { + mOutputStream.close(); + } + + @Override + public void flush() throws IOException { + if (mIsInterrupted) throw new InterruptedIOException(); + mOutputStream.flush(); + } + + public void interrupt() { + mIsInterrupted = true; + } +} diff --git a/src/com/android/gallery3d/util/LinkedNode.java b/src/com/android/gallery3d/util/LinkedNode.java new file mode 100644 index 000000000..8554acd21 --- /dev/null +++ b/src/com/android/gallery3d/util/LinkedNode.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + + +public class LinkedNode { + private LinkedNode mPrev; + private LinkedNode mNext; + + public LinkedNode() { + mPrev = mNext = this; + } + + public void insert(LinkedNode node) { + node.mNext = mNext; + mNext.mPrev = node; + node.mPrev = this; + mNext = node; + } + + public void remove() { + if (mNext == this) throw new IllegalStateException(); + mPrev.mNext = mNext; + mNext.mPrev = mPrev; + mPrev = mNext = null; + } + + @SuppressWarnings("unchecked") + public static class List { + private LinkedNode mHead = new LinkedNode(); + + public void insertFirst(T node) { + mHead.insert(node); + } + + public void insertLast(T node) { + mHead.mPrev.insert(node); + } + + public T getFirst() { + return (T) (mHead.mNext == mHead ? null : mHead.mNext); + } + + public T getLast() { + return (T) (mHead.mPrev == mHead ? null : mHead.mPrev); + } + + public T nextOf(T node) { + return (T) (node.mNext == mHead ? null : node.mNext); + } + + public T previousOf(T node) { + return (T) (node.mPrev == mHead ? null : node.mPrev); + } + + } + + public static List newList() { + return new List(); + } +} diff --git a/src/com/android/gallery3d/util/Log.java b/src/com/android/gallery3d/util/Log.java new file mode 100644 index 000000000..d7f8e85d0 --- /dev/null +++ b/src/com/android/gallery3d/util/Log.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +public class Log { + public static int v(String tag, String msg) { + return android.util.Log.v(tag, msg); + } + public static int v(String tag, String msg, Throwable tr) { + return android.util.Log.v(tag, msg, tr); + } + public static int d(String tag, String msg) { + return android.util.Log.d(tag, msg); + } + public static int d(String tag, String msg, Throwable tr) { + return android.util.Log.d(tag, msg, tr); + } + public static int i(String tag, String msg) { + return android.util.Log.i(tag, msg); + } + public static int i(String tag, String msg, Throwable tr) { + return android.util.Log.i(tag, msg, tr); + } + public static int w(String tag, String msg) { + return android.util.Log.w(tag, msg); + } + public static int w(String tag, String msg, Throwable tr) { + return android.util.Log.w(tag, msg, tr); + } + public static int w(String tag, Throwable tr) { + return android.util.Log.w(tag, tr); + } + public static int e(String tag, String msg) { + return android.util.Log.e(tag, msg); + } + public static int e(String tag, String msg, Throwable tr) { + return android.util.Log.e(tag, msg, tr); + } +} diff --git a/src/com/android/gallery3d/util/MediaSetUtils.java b/src/com/android/gallery3d/util/MediaSetUtils.java new file mode 100644 index 000000000..817ffedcb --- /dev/null +++ b/src/com/android/gallery3d/util/MediaSetUtils.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.MtpContext; +import com.android.gallery3d.data.Path; + +import android.os.Environment; + +import java.util.Comparator; + +public class MediaSetUtils { + public static final Comparator NAME_COMPARATOR = new NameComparator(); + + public static final int CAMERA_BUCKET_ID = GalleryUtils.getBucketId( + Environment.getExternalStorageDirectory().toString() + "/DCIM/Camera"); + public static final int DOWNLOAD_BUCKET_ID = GalleryUtils.getBucketId( + Environment.getExternalStorageDirectory().toString() + "/download"); + public static final int IMPORTED_BUCKET_ID = GalleryUtils.getBucketId( + Environment.getExternalStorageDirectory().toString() + "/" + + MtpContext.NAME_IMPORTED_FOLDER); + + private static final Path[] CAMERA_PATHS = { + Path.fromString("/local/all/" + CAMERA_BUCKET_ID), + Path.fromString("/local/image/" + CAMERA_BUCKET_ID), + Path.fromString("/local/video/" + CAMERA_BUCKET_ID)}; + + public static boolean isCameraSource(Path path) { + return CAMERA_PATHS[0] == path || CAMERA_PATHS[1] == path + || CAMERA_PATHS[2] == path; + } + + // Sort MediaSets by name + public static class NameComparator implements Comparator { + public int compare(MediaSet set1, MediaSet set2) { + int result = set1.getName().compareToIgnoreCase(set2.getName()); + if (result != 0) return result; + return set1.getPath().toString().compareTo(set2.getPath().toString()); + } + } +} diff --git a/src/com/android/gallery3d/util/PriorityThreadFactory.java b/src/com/android/gallery3d/util/PriorityThreadFactory.java new file mode 100644 index 000000000..67b215274 --- /dev/null +++ b/src/com/android/gallery3d/util/PriorityThreadFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.gallery3d.util; + + +import android.os.Process; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A thread factory that creates threads with a given thread priority. + */ +public class PriorityThreadFactory implements ThreadFactory { + + private final int mPriority; + private final AtomicInteger mNumber = new AtomicInteger(); + private final String mName; + + public PriorityThreadFactory(String name, int priority) { + mName = name; + mPriority = priority; + } + + public Thread newThread(Runnable r) { + return new Thread(r, mName + '-' + mNumber.getAndIncrement()) { + @Override + public void run() { + Process.setThreadPriority(mPriority); + super.run(); + } + }; + } + +} diff --git a/src/com/android/gallery3d/util/ReverseGeocoder.java b/src/com/android/gallery3d/util/ReverseGeocoder.java new file mode 100644 index 000000000..d253b4b96 --- /dev/null +++ b/src/com/android/gallery3d/util/ReverseGeocoder.java @@ -0,0 +1,417 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +import com.android.gallery3d.common.BlobCache; + +import android.content.Context; +import android.location.Address; +import android.location.Geocoder; +import android.location.Location; +import android.location.LocationManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +public class ReverseGeocoder { + private static final String TAG = "ReverseGeocoder"; + public static final int EARTH_RADIUS_METERS = 6378137; + public static final int LAT_MIN = -90; + public static final int LAT_MAX = 90; + public static final int LON_MIN = -180; + public static final int LON_MAX = 180; + private static final int MAX_COUNTRY_NAME_LENGTH = 8; + // If two points are within 20 miles of each other, use + // "Around Palo Alto, CA" or "Around Mountain View, CA". + // instead of directly jumping to the next level and saying + // "California, US". + private static final int MAX_LOCALITY_MILE_RANGE = 20; + + private static final String GEO_CACHE_FILE = "rev_geocoding"; + private static final int GEO_CACHE_MAX_ENTRIES = 1000; + private static final int GEO_CACHE_MAX_BYTES = 500 * 1024; + private static final int GEO_CACHE_VERSION = 0; + + public static class SetLatLong { + // The latitude and longitude of the min latitude point. + public double mMinLatLatitude = LAT_MAX; + public double mMinLatLongitude; + // The latitude and longitude of the max latitude point. + public double mMaxLatLatitude = LAT_MIN; + public double mMaxLatLongitude; + // The latitude and longitude of the min longitude point. + public double mMinLonLatitude; + public double mMinLonLongitude = LON_MAX; + // The latitude and longitude of the max longitude point. + public double mMaxLonLatitude; + public double mMaxLonLongitude = LON_MIN; + } + + private Context mContext; + private Geocoder mGeocoder; + private BlobCache mGeoCache; + private ConnectivityManager mConnectivityManager; + private static Address sCurrentAddress; // last known address + + public ReverseGeocoder(Context context) { + mContext = context; + mGeocoder = new Geocoder(mContext); + mGeoCache = CacheManager.getCache(context, GEO_CACHE_FILE, + GEO_CACHE_MAX_ENTRIES, GEO_CACHE_MAX_BYTES, + GEO_CACHE_VERSION); + mConnectivityManager = (ConnectivityManager) + context.getSystemService(Context.CONNECTIVITY_SERVICE); + } + + public String computeAddress(SetLatLong set) { + // The overall min and max latitudes and longitudes of the set. + double setMinLatitude = set.mMinLatLatitude; + double setMinLongitude = set.mMinLatLongitude; + double setMaxLatitude = set.mMaxLatLatitude; + double setMaxLongitude = set.mMaxLatLongitude; + if (Math.abs(set.mMaxLatLatitude - set.mMinLatLatitude) + < Math.abs(set.mMaxLonLongitude - set.mMinLonLongitude)) { + setMinLatitude = set.mMinLonLatitude; + setMinLongitude = set.mMinLonLongitude; + setMaxLatitude = set.mMaxLonLatitude; + setMaxLongitude = set.mMaxLonLongitude; + } + Address addr1 = lookupAddress(setMinLatitude, setMinLongitude, true); + Address addr2 = lookupAddress(setMaxLatitude, setMaxLongitude, true); + if (addr1 == null) + addr1 = addr2; + if (addr2 == null) + addr2 = addr1; + if (addr1 == null || addr2 == null) { + return null; + } + + // Get current location, we decide the granularity of the string based + // on this. + LocationManager locationManager = + (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); + Location location = null; + List providers = locationManager.getAllProviders(); + for (int i = 0; i < providers.size(); ++i) { + String provider = providers.get(i); + location = (provider != null) ? locationManager.getLastKnownLocation(provider) : null; + if (location != null) + break; + } + String currentCity = ""; + String currentAdminArea = ""; + String currentCountry = Locale.getDefault().getCountry(); + if (location != null) { + Address currentAddress = lookupAddress( + location.getLatitude(), location.getLongitude(), true); + if (currentAddress == null) { + currentAddress = sCurrentAddress; + } else { + sCurrentAddress = currentAddress; + } + if (currentAddress != null && currentAddress.getCountryCode() != null) { + currentCity = checkNull(currentAddress.getLocality()); + currentCountry = checkNull(currentAddress.getCountryCode()); + currentAdminArea = checkNull(currentAddress.getAdminArea()); + } + } + + String closestCommonLocation = null; + String addr1Locality = checkNull(addr1.getLocality()); + String addr2Locality = checkNull(addr2.getLocality()); + String addr1AdminArea = checkNull(addr1.getAdminArea()); + String addr2AdminArea = checkNull(addr2.getAdminArea()); + String addr1CountryCode = checkNull(addr1.getCountryCode()); + String addr2CountryCode = checkNull(addr2.getCountryCode()); + + if (currentCity.equals(addr1Locality) || currentCity.equals(addr2Locality)) { + String otherCity = currentCity; + if (currentCity.equals(addr1Locality)) { + otherCity = addr2Locality; + if (otherCity.length() == 0) { + otherCity = addr2AdminArea; + if (!currentCountry.equals(addr2CountryCode)) { + otherCity += " " + addr2CountryCode; + } + } + addr2Locality = addr1Locality; + addr2AdminArea = addr1AdminArea; + addr2CountryCode = addr1CountryCode; + } else { + otherCity = addr1Locality; + if (otherCity.length() == 0) { + otherCity = addr1AdminArea; + if (!currentCountry.equals(addr1CountryCode)) { + otherCity += " " + addr1CountryCode; + } + } + addr1Locality = addr2Locality; + addr1AdminArea = addr2AdminArea; + addr1CountryCode = addr2CountryCode; + } + closestCommonLocation = valueIfEqual(addr1.getAddressLine(0), addr2.getAddressLine(0)); + if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) { + if (!currentCity.equals(otherCity)) { + closestCommonLocation += " - " + otherCity; + } + return closestCommonLocation; + } + + // Compare thoroughfare (street address) next. + closestCommonLocation = valueIfEqual(addr1.getThoroughfare(), addr2.getThoroughfare()); + if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) { + return closestCommonLocation; + } + } + + // Compare the locality. + closestCommonLocation = valueIfEqual(addr1Locality, addr2Locality); + if (closestCommonLocation != null && !("".equals(closestCommonLocation))) { + String adminArea = addr1AdminArea; + String countryCode = addr1CountryCode; + if (adminArea != null && adminArea.length() > 0) { + if (!countryCode.equals(currentCountry)) { + closestCommonLocation += ", " + adminArea + " " + countryCode; + } else { + closestCommonLocation += ", " + adminArea; + } + } + return closestCommonLocation; + } + + // If the admin area is the same as the current location, we hide it and + // instead show the city name. + if (currentAdminArea.equals(addr1AdminArea) && currentAdminArea.equals(addr2AdminArea)) { + if ("".equals(addr1Locality)) { + addr1Locality = addr2Locality; + } + if ("".equals(addr2Locality)) { + addr2Locality = addr1Locality; + } + if (!"".equals(addr1Locality)) { + if (addr1Locality.equals(addr2Locality)) { + closestCommonLocation = addr1Locality + ", " + currentAdminArea; + } else { + closestCommonLocation = addr1Locality + " - " + addr2Locality; + } + return closestCommonLocation; + } + } + + // Just choose one of the localities if within a MAX_LOCALITY_MILE_RANGE + // mile radius. + float[] distanceFloat = new float[1]; + Location.distanceBetween(setMinLatitude, setMinLongitude, + setMaxLatitude, setMaxLongitude, distanceFloat); + int distance = (int) GalleryUtils.toMile(distanceFloat[0]); + if (distance < MAX_LOCALITY_MILE_RANGE) { + // Try each of the points and just return the first one to have a + // valid address. + closestCommonLocation = getLocalityAdminForAddress(addr1, true); + if (closestCommonLocation != null) { + return closestCommonLocation; + } + closestCommonLocation = getLocalityAdminForAddress(addr2, true); + if (closestCommonLocation != null) { + return closestCommonLocation; + } + } + + // Check the administrative area. + closestCommonLocation = valueIfEqual(addr1AdminArea, addr2AdminArea); + if (closestCommonLocation != null && !("".equals(closestCommonLocation))) { + String countryCode = addr1CountryCode; + if (!countryCode.equals(currentCountry)) { + if (countryCode != null && countryCode.length() > 0) { + closestCommonLocation += " " + countryCode; + } + } + return closestCommonLocation; + } + + // Check the country codes. + closestCommonLocation = valueIfEqual(addr1CountryCode, addr2CountryCode); + if (closestCommonLocation != null && !("".equals(closestCommonLocation))) { + return closestCommonLocation; + } + // There is no intersection, let's choose a nicer name. + String addr1Country = addr1.getCountryName(); + String addr2Country = addr2.getCountryName(); + if (addr1Country == null) + addr1Country = addr1CountryCode; + if (addr2Country == null) + addr2Country = addr2CountryCode; + if (addr1Country == null || addr2Country == null) + return null; + if (addr1Country.length() > MAX_COUNTRY_NAME_LENGTH || addr2Country.length() > MAX_COUNTRY_NAME_LENGTH) { + closestCommonLocation = addr1CountryCode + " - " + addr2CountryCode; + } else { + closestCommonLocation = addr1Country + " - " + addr2Country; + } + return closestCommonLocation; + } + + private String checkNull(String locality) { + if (locality == null) + return ""; + if (locality.equals("null")) + return ""; + return locality; + } + + private String getLocalityAdminForAddress(final Address addr, final boolean approxLocation) { + if (addr == null) + return ""; + String localityAdminStr = addr.getLocality(); + if (localityAdminStr != null && !("null".equals(localityAdminStr))) { + if (approxLocation) { + // TODO: Uncomment these lines as soon as we may translations + // for Res.string.around. + // localityAdminStr = + // mContext.getResources().getString(Res.string.around) + " " + + // localityAdminStr; + } + String adminArea = addr.getAdminArea(); + if (adminArea != null && adminArea.length() > 0) { + localityAdminStr += ", " + adminArea; + } + return localityAdminStr; + } + return null; + } + + public Address lookupAddress(final double latitude, final double longitude, + boolean useCache) { + try { + long locationKey = (long) (((latitude + LAT_MAX) * 2 * LAT_MAX + + (longitude + LON_MAX)) * EARTH_RADIUS_METERS); + byte[] cachedLocation = null; + if (useCache && mGeoCache != null) { + cachedLocation = mGeoCache.lookup(locationKey); + } + Address address = null; + NetworkInfo networkInfo = mConnectivityManager.getActiveNetworkInfo(); + if (cachedLocation == null || cachedLocation.length == 0) { + if (networkInfo == null || !networkInfo.isConnected()) { + return null; + } + List
addresses = mGeocoder.getFromLocation(latitude, longitude, 1); + if (!addresses.isEmpty()) { + address = addresses.get(0); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + Locale locale = address.getLocale(); + writeUTF(dos, locale.getLanguage()); + writeUTF(dos, locale.getCountry()); + writeUTF(dos, locale.getVariant()); + + writeUTF(dos, address.getThoroughfare()); + int numAddressLines = address.getMaxAddressLineIndex(); + dos.writeInt(numAddressLines); + for (int i = 0; i < numAddressLines; ++i) { + writeUTF(dos, address.getAddressLine(i)); + } + writeUTF(dos, address.getFeatureName()); + writeUTF(dos, address.getLocality()); + writeUTF(dos, address.getAdminArea()); + writeUTF(dos, address.getSubAdminArea()); + + writeUTF(dos, address.getCountryName()); + writeUTF(dos, address.getCountryCode()); + writeUTF(dos, address.getPostalCode()); + writeUTF(dos, address.getPhone()); + writeUTF(dos, address.getUrl()); + + dos.flush(); + if (mGeoCache != null) { + mGeoCache.insert(locationKey, bos.toByteArray()); + } + dos.close(); + } + } else { + // Parsing the address from the byte stream. + DataInputStream dis = new DataInputStream( + new ByteArrayInputStream(cachedLocation)); + String language = readUTF(dis); + String country = readUTF(dis); + String variant = readUTF(dis); + Locale locale = null; + if (language != null) { + if (country == null) { + locale = new Locale(language); + } else if (variant == null) { + locale = new Locale(language, country); + } else { + locale = new Locale(language, country, variant); + } + } + if (!locale.getLanguage().equals(Locale.getDefault().getLanguage())) { + dis.close(); + return lookupAddress(latitude, longitude, false); + } + address = new Address(locale); + + address.setThoroughfare(readUTF(dis)); + int numAddressLines = dis.readInt(); + for (int i = 0; i < numAddressLines; ++i) { + address.setAddressLine(i, readUTF(dis)); + } + address.setFeatureName(readUTF(dis)); + address.setLocality(readUTF(dis)); + address.setAdminArea(readUTF(dis)); + address.setSubAdminArea(readUTF(dis)); + + address.setCountryName(readUTF(dis)); + address.setCountryCode(readUTF(dis)); + address.setPostalCode(readUTF(dis)); + address.setPhone(readUTF(dis)); + address.setUrl(readUTF(dis)); + dis.close(); + } + return address; + } catch (Exception e) { + // Ignore. + } + return null; + } + + private String valueIfEqual(String a, String b) { + return (a != null && b != null && a.equalsIgnoreCase(b)) ? a : null; + } + + public static final void writeUTF(DataOutputStream dos, String string) throws IOException { + if (string == null) { + dos.writeUTF(""); + } else { + dos.writeUTF(string); + } + } + + public static final String readUTF(DataInputStream dis) throws IOException { + String retVal = dis.readUTF(); + if (retVal.length() == 0) + return null; + return retVal; + } +} diff --git a/src/com/android/gallery3d/util/ThreadPool.java b/src/com/android/gallery3d/util/ThreadPool.java new file mode 100644 index 000000000..71bb3c5b7 --- /dev/null +++ b/src/com/android/gallery3d/util/ThreadPool.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ThreadPool { + private static final String TAG = "ThreadPool"; + private static final int CORE_POOL_SIZE = 4; + private static final int MAX_POOL_SIZE = 8; + private static final int KEEP_ALIVE_TIME = 10; // 10 seconds + + // Resource type + public static final int MODE_NONE = 0; + public static final int MODE_CPU = 1; + public static final int MODE_NETWORK = 2; + + public static final JobContext JOB_CONTEXT_STUB = new JobContextStub(); + + ResourceCounter mCpuCounter = new ResourceCounter(2); + ResourceCounter mNetworkCounter = new ResourceCounter(2); + + // A Job is like a Callable, but it has an addition JobContext parameter. + public interface Job { + public T run(JobContext jc); + } + + public interface JobContext { + boolean isCancelled(); + void setCancelListener(CancelListener listener); + boolean setMode(int mode); + } + + private static class JobContextStub implements JobContext { + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void setCancelListener(CancelListener listener) { + } + + @Override + public boolean setMode(int mode) { + return true; + } + } + + public interface CancelListener { + public void onCancel(); + } + + private static class ResourceCounter { + public int value; + public ResourceCounter(int v) { + value = v; + } + } + + private final Executor mExecutor; + + public ThreadPool() { + mExecutor = new ThreadPoolExecutor( + CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, + TimeUnit.SECONDS, new LinkedBlockingQueue(), + new PriorityThreadFactory("thread-pool", + android.os.Process.THREAD_PRIORITY_BACKGROUND)); + } + + // Submit a job to the thread pool. The listener will be called when the + // job is finished (or cancelled). + public Future submit(Job job, FutureListener listener) { + Worker w = new Worker(job, listener); + mExecutor.execute(w); + return w; + } + + public Future submit(Job job) { + return submit(job, null); + } + + private class Worker implements Runnable, Future, JobContext { + private static final String TAG = "Worker"; + private Job mJob; + private FutureListener mListener; + private CancelListener mCancelListener; + private ResourceCounter mWaitOnResource; + private volatile boolean mIsCancelled; + private boolean mIsDone; + private T mResult; + private int mMode; + + public Worker(Job job, FutureListener listener) { + mJob = job; + mListener = listener; + } + + // This is called by a thread in the thread pool. + public void run() { + T result = null; + + // A job is in CPU mode by default. setMode returns false + // if the job is cancelled. + if (setMode(MODE_CPU)) { + try { + result = mJob.run(this); + } catch (Throwable ex) { + Log.w(TAG, "Exception in running a job", ex); + } + } + + synchronized(this) { + setMode(MODE_NONE); + mResult = result; + mIsDone = true; + notifyAll(); + } + if (mListener != null) mListener.onFutureDone(this); + } + + // Below are the methods for Future. + public synchronized void cancel() { + if (mIsCancelled) return; + mIsCancelled = true; + if (mWaitOnResource != null) { + synchronized (mWaitOnResource) { + mWaitOnResource.notifyAll(); + } + } + if (mCancelListener != null) { + mCancelListener.onCancel(); + } + } + + public boolean isCancelled() { + return mIsCancelled; + } + + public synchronized boolean isDone() { + return mIsDone; + } + + public synchronized T get() { + while (!mIsDone) { + try { + wait(); + } catch (Exception ex) { + Log.w(TAG, "ingore exception", ex); + // ignore. + } + } + return mResult; + } + + public void waitDone() { + get(); + } + + // Below are the methods for JobContext (only called from the + // thread running the job) + public synchronized void setCancelListener(CancelListener listener) { + mCancelListener = listener; + if (mIsCancelled && mCancelListener != null) { + mCancelListener.onCancel(); + } + } + + public boolean setMode(int mode) { + // Release old resource + ResourceCounter rc = modeToCounter(mMode); + if (rc != null) releaseResource(rc); + mMode = MODE_NONE; + + // Acquire new resource + rc = modeToCounter(mode); + if (rc != null) { + if (!acquireResource(rc)) { + return false; + } + mMode = mode; + } + + return true; + } + + private ResourceCounter modeToCounter(int mode) { + if (mode == MODE_CPU) { + return mCpuCounter; + } else if (mode == MODE_NETWORK) { + return mNetworkCounter; + } else { + return null; + } + } + + private boolean acquireResource(ResourceCounter counter) { + while (true) { + synchronized (this) { + if (mIsCancelled) { + mWaitOnResource = null; + return false; + } + mWaitOnResource = counter; + } + + synchronized (counter) { + if (counter.value > 0) { + counter.value--; + break; + } else { + try { + counter.wait(); + } catch (InterruptedException ex) { + // ignore. + } + } + } + } + + synchronized (this) { + mWaitOnResource = null; + } + + return true; + } + + private void releaseResource(ResourceCounter counter) { + synchronized (counter) { + counter.value++; + counter.notifyAll(); + } + } + } +} diff --git a/src/com/android/gallery3d/util/UpdateHelper.java b/src/com/android/gallery3d/util/UpdateHelper.java new file mode 100644 index 000000000..9fdade683 --- /dev/null +++ b/src/com/android/gallery3d/util/UpdateHelper.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.gallery3d.util; + +import com.android.gallery3d.common.Utils; + +public class UpdateHelper { + + private boolean mUpdated = false; + + public int update(int original, int update) { + if (original != update) { + mUpdated = true; + original = update; + } + return original; + } + + public long update(long original, long update) { + if (original != update) { + mUpdated = true; + original = update; + } + return original; + } + + public double update(double original, double update) { + if (original != update) { + mUpdated = true; + original = update; + } + return original; + } + + public double update(float original, float update) { + if (original != update) { + mUpdated = true; + original = update; + } + return original; + } + + public T update(T original, T update) { + if (!Utils.equals(original, update)) { + mUpdated = true; + original = update; + } + return original; + } + + public boolean isUpdated() { + return mUpdated; + } +} diff --git a/src/com/android/gallery3d/widget/LocalPhotoSource.java b/src/com/android/gallery3d/widget/LocalPhotoSource.java new file mode 100644 index 000000000..de16a7129 --- /dev/null +++ b/src/com/android/gallery3d/widget/LocalPhotoSource.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.widget; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Environment; +import android.os.Handler; +import android.provider.MediaStore.Images.Media; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Random; + +public class LocalPhotoSource implements WidgetSource { + + private static final String TAG = "LocalPhotoSource"; + + private static final int MAX_PHOTO_COUNT = 128; + + /* Static fields used to query for the correct set of images */ + private static final Uri CONTENT_URI = Media.EXTERNAL_CONTENT_URI; + private static final String DATE_TAKEN = Media.DATE_TAKEN; + private static final String[] PROJECTION = {Media._ID}; + private static final String[] COUNT_PROJECTION = {"count(*)"}; + /* We don't want to include the download directory */ + private static final String SELECTION = + String.format("%s != %s", Media.BUCKET_ID, getDownloadBucketId()); + private static final String ORDER = String.format("%s DESC", DATE_TAKEN); + + private Context mContext; + private ArrayList mPhotos = new ArrayList(); + private ContentListener mContentListener; + private ContentObserver mContentObserver; + private boolean mContentDirty = true; + private DataManager mDataManager; + private static final Path LOCAL_IMAGE_ROOT = Path.fromString("/local/image/item"); + + public LocalPhotoSource(Context context) { + mContext = context; + mDataManager = ((GalleryApp) context.getApplicationContext()).getDataManager(); + mContentObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + mContentDirty = true; + if (mContentListener != null) mContentListener.onContentDirty(); + } + }; + mContext.getContentResolver() + .registerContentObserver(CONTENT_URI, true, mContentObserver); + } + + public void close() { + mContext.getContentResolver().unregisterContentObserver(mContentObserver); + } + + @Override + public Uri getContentUri(int index) { + if (index < mPhotos.size()) { + return CONTENT_URI.buildUpon() + .appendPath(String.valueOf(mPhotos.get(index))) + .build(); + } + return null; + } + + @Override + public Bitmap getImage(int index) { + if (index >= mPhotos.size()) return null; + long id = mPhotos.get(index); + MediaItem image = (MediaItem) + mDataManager.getMediaObject(LOCAL_IMAGE_ROOT.getChild(id)); + if (image == null) return null; + + return WidgetUtils.createWidgetBitmap(image); + } + + private int[] getExponentialIndice(int total, int count) { + Random random = new Random(); + if (count > total) count = total; + HashSet selected = new HashSet(count); + while (selected.size() < count) { + int row = (int)(-Math.log(random.nextDouble()) * total / 2); + if (row < total) selected.add(row); + } + int values[] = new int[count]; + int index = 0; + for (int value : selected) { + values[index++] = value; + } + return values; + } + + private int getPhotoCount(ContentResolver resolver) { + Cursor cursor = resolver.query( + CONTENT_URI, COUNT_PROJECTION, SELECTION, null, null); + if (cursor == null) return 0; + try { + Utils.assertTrue(cursor.moveToNext()); + return cursor.getInt(0); + } finally { + cursor.close(); + } + } + + private boolean isContentSound(int totalCount) { + if (mPhotos.size() < Math.min(totalCount, MAX_PHOTO_COUNT)) return false; + if (mPhotos.size() == 0) return true; // totalCount is also 0 + + StringBuilder builder = new StringBuilder(); + for (Long imageId : mPhotos) { + if (builder.length() > 0) builder.append(","); + builder.append(imageId); + } + Cursor cursor = mContext.getContentResolver().query( + CONTENT_URI, COUNT_PROJECTION, + String.format("%s in (%s)", Media._ID, builder.toString()), + null, null); + if (cursor == null) return false; + try { + Utils.assertTrue(cursor.moveToNext()); + return cursor.getInt(0) == mPhotos.size(); + } finally { + cursor.close(); + } + } + + public void reload() { + if (!mContentDirty) return; + mContentDirty = false; + + ContentResolver resolver = mContext.getContentResolver(); + int photoCount = getPhotoCount(resolver); + if (isContentSound(photoCount)) return; + + int choosedIds[] = getExponentialIndice(photoCount, MAX_PHOTO_COUNT); + Arrays.sort(choosedIds); + + mPhotos.clear(); + Cursor cursor = mContext.getContentResolver().query( + CONTENT_URI, PROJECTION, SELECTION, null, ORDER); + if (cursor == null) return; + try { + for (int index : choosedIds) { + if (cursor.moveToPosition(index)) { + mPhotos.add(cursor.getLong(0)); + } + } + } finally { + cursor.close(); + } + } + + @Override + public int size() { + reload(); + return mPhotos.size(); + } + + /** + * Builds the bucket ID for the public external storage Downloads directory + * @return the bucket ID + */ + private static int getDownloadBucketId() { + String downloadsPath = Environment + .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + .getAbsolutePath(); + return GalleryUtils.getBucketId(downloadsPath); + } + + @Override + public void setContentListener(ContentListener listener) { + mContentListener = listener; + } +} diff --git a/src/com/android/gallery3d/widget/MediaSetSource.java b/src/com/android/gallery3d/widget/MediaSetSource.java new file mode 100644 index 000000000..1677f69f1 --- /dev/null +++ b/src/com/android/gallery3d/widget/MediaSetSource.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.widget; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Binder; + +import java.util.ArrayList; +import java.util.Arrays; + +public class MediaSetSource implements WidgetSource, ContentListener { + private static final int CACHE_SIZE = 32; + + private static final String TAG = "MediaSetSource"; + + private MediaSet mSource; + private MediaItem mCache[] = new MediaItem[CACHE_SIZE]; + private int mCacheStart; + private int mCacheEnd; + private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; + + private ContentListener mContentListener; + + public MediaSetSource(MediaSet source) { + mSource = Utils.checkNotNull(source); + mSource.addContentListener(this); + } + + @Override + public void close() { + mSource.removeContentListener(this); + } + + private void ensureCacheRange(int index) { + if (index >= mCacheStart && index < mCacheEnd) return; + + long token = Binder.clearCallingIdentity(); + try { + mCacheStart = index; + ArrayList items = mSource.getMediaItem(mCacheStart, CACHE_SIZE); + mCacheEnd = mCacheStart + items.size(); + items.toArray(mCache); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public synchronized Uri getContentUri(int index) { + ensureCacheRange(index); + if (index < mCacheStart || index >= mCacheEnd) return null; + return mCache[index - mCacheStart].getContentUri(); + } + + @Override + public synchronized Bitmap getImage(int index) { + ensureCacheRange(index); + if (index < mCacheStart || index >= mCacheEnd) return null; + return WidgetUtils.createWidgetBitmap(mCache[index - mCacheStart]); + } + + @Override + public void reload() { + long version = mSource.reload(); + if (mSourceVersion != version) { + mSourceVersion = version; + mCacheStart = 0; + mCacheEnd = 0; + Arrays.fill(mCache, null); + } + } + + @Override + public void setContentListener(ContentListener listener) { + mContentListener = listener; + } + + @Override + public int size() { + long token = Binder.clearCallingIdentity(); + try { + return mSource.getMediaItemCount(); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void onContentDirty() { + if (mContentListener != null) mContentListener.onContentDirty(); + } +} diff --git a/src/com/android/gallery3d/widget/WidgetClickHandler.java b/src/com/android/gallery3d/widget/WidgetClickHandler.java new file mode 100644 index 000000000..362e4d20c --- /dev/null +++ b/src/com/android/gallery3d/widget/WidgetClickHandler.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.widget; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.Gallery; + +import android.app.Activity; +import android.content.Intent; +import android.content.res.AssetFileDescriptor; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; + +public class WidgetClickHandler extends Activity { + private static final String TAG = "PhotoAppWidgetClickHandler"; + + private boolean isValidDataUri(Uri dataUri) { + if (dataUri == null) return false; + try { + AssetFileDescriptor f = getContentResolver() + .openAssetFileDescriptor(dataUri, "r"); + f.close(); + return true; + } catch (Throwable e) { + Log.w(TAG, "cannot open uri: " + dataUri, e); + return false; + } + } + + @Override + protected void onCreate(Bundle savedState) { + super.onCreate(savedState); + Intent intent = getIntent(); + if (isValidDataUri(intent.getData())) { + startActivity(new Intent(Intent.ACTION_VIEW, intent.getData())); + } else { + Toast.makeText(this, + R.string.no_such_item, Toast.LENGTH_LONG).show(); + startActivity(new Intent(this, Gallery.class)); + } + finish(); + } +} diff --git a/src/com/android/gallery3d/widget/WidgetConfigure.java b/src/com/android/gallery3d/widget/WidgetConfigure.java new file mode 100644 index 000000000..3bcd9c46e --- /dev/null +++ b/src/com/android/gallery3d/widget/WidgetConfigure.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.widget; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AlbumPicker; +import com.android.gallery3d.app.CropImage; +import com.android.gallery3d.app.DialogPicker; + +import android.app.Activity; +import android.appwidget.AppWidgetManager; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.widget.RemoteViews; + +public class WidgetConfigure extends Activity { + @SuppressWarnings("unused") + private static final String TAG = "WidgetConfigure"; + + public static final String KEY_WIDGET_TYPE = "widget-type"; + + private static final int REQUEST_WIDGET_TYPE = 1; + private static final int REQUEST_CHOOSE_ALBUM = 2; + private static final int REQUEST_CROP_IMAGE = 3; + private static final int REQUEST_GET_PHOTO = 4; + + public static final int RESULT_ERROR = RESULT_FIRST_USER; + + // Scale up the widget size since we only specified the minimized + // size of the gadget. The real size could be larger. + // Note: There is also a limit on the size of data that can be + // passed in Binder's transaction. + private static float WIDGET_SCALE_FACTOR = 1.5f; + + private int mAppWidgetId = -1; + private int mWidgetType = 0; + private Uri mPickedItem; + + @Override + protected void onCreate(Bundle bundle) { + super.onCreate(bundle); + mAppWidgetId = getIntent().getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); + + if (mAppWidgetId == -1) { + setResult(Activity.RESULT_CANCELED); + finish(); + return; + } + + if (mWidgetType == 0) { + Intent intent = new Intent(this, WidgetTypeChooser.class); + startActivityForResult(intent, REQUEST_WIDGET_TYPE); + } + } + + private void updateWidgetAndFinish(WidgetDatabaseHelper.Entry entry) { + AppWidgetManager manager = AppWidgetManager.getInstance(this); + RemoteViews views = WidgetProvider.buildWidget(this, mAppWidgetId, entry); + manager.updateAppWidget(mAppWidgetId, views); + setResult(RESULT_OK, new Intent().putExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId)); + finish(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode != RESULT_OK) { + setResult(resultCode, new Intent().putExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId)); + finish(); + return; + } + + if (requestCode == REQUEST_WIDGET_TYPE) { + setWidgetType(data); + } else if (requestCode == REQUEST_CHOOSE_ALBUM) { + setChoosenAlbum(data); + } else if (requestCode == REQUEST_GET_PHOTO) { + setChoosenPhoto(data); + } else if (requestCode == REQUEST_CROP_IMAGE) { + setPhotoWidget(data); + } else { + throw new AssertionError("unknown request: " + requestCode); + } + } + + private void setPhotoWidget(Intent data) { + // Store the cropped photo in our database + Bitmap bitmap = (Bitmap) data.getParcelableExtra("data"); + WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this); + try { + helper.setPhoto(mAppWidgetId, mPickedItem, bitmap); + updateWidgetAndFinish(helper.getEntry(mAppWidgetId)); + } finally { + helper.close(); + } + } + + private void setChoosenPhoto(Intent data) { + Resources res = getResources(); + int widgetWidth = Math.round(WIDGET_SCALE_FACTOR + * res.getDimension(R.dimen.appwidget_width)); + int widgetHeight = Math.round(WIDGET_SCALE_FACTOR + * res.getDimension(R.dimen.appwidget_height)); + mPickedItem = data.getData(); + Intent request = new Intent(CropImage.ACTION_CROP, mPickedItem) + .putExtra(CropImage.KEY_OUTPUT_X, widgetWidth) + .putExtra(CropImage.KEY_OUTPUT_Y, widgetHeight) + .putExtra(CropImage.KEY_ASPECT_X, widgetWidth) + .putExtra(CropImage.KEY_ASPECT_Y, widgetHeight) + .putExtra(CropImage.KEY_SCALE_UP_IF_NEEDED, true) + .putExtra(CropImage.KEY_SCALE, true) + .putExtra(CropImage.KEY_RETURN_DATA, true); + startActivityForResult(request, REQUEST_CROP_IMAGE); + } + + private void setChoosenAlbum(Intent data) { + String albumPath = data.getStringExtra(AlbumPicker.KEY_ALBUM_PATH); + WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this); + try { + helper.setWidget(mAppWidgetId, + WidgetDatabaseHelper.TYPE_ALBUM, albumPath); + updateWidgetAndFinish(helper.getEntry(mAppWidgetId)); + } finally { + helper.close(); + } + } + + private void setWidgetType(Intent data) { + mWidgetType = data.getIntExtra(KEY_WIDGET_TYPE, R.id.widget_type_shuffle); + if (mWidgetType == R.id.widget_type_album) { + Intent intent = new Intent(this, AlbumPicker.class); + startActivityForResult(intent, REQUEST_CHOOSE_ALBUM); + } else if (mWidgetType == R.id.widget_type_shuffle) { + WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this); + try { + helper.setWidget(mAppWidgetId, WidgetDatabaseHelper.TYPE_SHUFFLE, null); + updateWidgetAndFinish(helper.getEntry(mAppWidgetId)); + } finally { + helper.close(); + } + } else { + // Explicitly send the intent to the DialogPhotoPicker + Intent request = new Intent(this, DialogPicker.class) + .setAction(Intent.ACTION_GET_CONTENT) + .setType("image/*"); + startActivityForResult(request, REQUEST_GET_PHOTO); + } + } +} diff --git a/src/com/android/gallery3d/widget/WidgetDatabaseHelper.java b/src/com/android/gallery3d/widget/WidgetDatabaseHelper.java new file mode 100644 index 000000000..d5bf22e18 --- /dev/null +++ b/src/com/android/gallery3d/widget/WidgetDatabaseHelper.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.widget; + +import com.android.gallery3d.common.Utils; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteOpenHelper; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.Log; + +import java.io.ByteArrayOutputStream; + +public class WidgetDatabaseHelper extends SQLiteOpenHelper { + private static final String TAG = "PhotoDatabaseHelper"; + private static final String DATABASE_NAME = "launcher.db"; + + private static final int DATABASE_VERSION = 4; + + private static final String TABLE_WIDGETS = "widgets"; + + private static final String FIELD_APPWIDGET_ID = "appWidgetId"; + private static final String FIELD_IMAGE_URI = "imageUri"; + private static final String FIELD_PHOTO_BLOB = "photoBlob"; + private static final String FIELD_WIDGET_TYPE = "widgetType"; + private static final String FIELD_ALBUM_PATH = "albumPath"; + + public static final int TYPE_SINGLE_PHOTO = 0; + public static final int TYPE_SHUFFLE = 1; + public static final int TYPE_ALBUM = 2; + + private static final String[] PROJECTION = { + FIELD_WIDGET_TYPE, FIELD_IMAGE_URI, FIELD_PHOTO_BLOB, FIELD_ALBUM_PATH}; + private static final int INDEX_WIDGET_TYPE = 0; + private static final int INDEX_IMAGE_URI = 1; + private static final int INDEX_PHOTO_BLOB = 2; + private static final int INDEX_ALBUM_PATH = 3; + private static final String WHERE_CLAUSE = FIELD_APPWIDGET_ID + " = ?"; + + public static class Entry { + public int widgetId; + public int type; + public Uri imageUri; + public Bitmap image; + public String albumPath; + + private Entry(int id, Cursor cursor) { + widgetId = id; + type = cursor.getInt(INDEX_WIDGET_TYPE); + + if (type == TYPE_SINGLE_PHOTO) { + imageUri = Uri.parse(cursor.getString(INDEX_IMAGE_URI)); + image = loadBitmap(cursor, INDEX_PHOTO_BLOB); + } else if (type == TYPE_ALBUM) { + albumPath = cursor.getString(INDEX_ALBUM_PATH); + } + } + } + + public WidgetDatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_WIDGETS + " (" + + FIELD_APPWIDGET_ID + " INTEGER PRIMARY KEY, " + + FIELD_WIDGET_TYPE + " INTEGER DEFAULT 0, " + + FIELD_IMAGE_URI + " TEXT, " + + FIELD_ALBUM_PATH + " TEXT, " + + FIELD_PHOTO_BLOB + " BLOB)"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + int version = oldVersion; + + if (version != DATABASE_VERSION) { + Log.w(TAG, "destroying all old data."); + // Table "photos" is renamed to "widget" in version 4 + db.execSQL("DROP TABLE IF EXISTS photos"); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_WIDGETS); + onCreate(db); + } + } + + /** + * Store the given bitmap in this database for the given appWidgetId. + */ + public boolean setPhoto(int appWidgetId, Uri imageUri, Bitmap bitmap) { + try { + // Try go guesstimate how much space the icon will take when + // serialized to avoid unnecessary allocations/copies during + // the write. + int size = bitmap.getWidth() * bitmap.getHeight() * 4; + ByteArrayOutputStream out = new ByteArrayOutputStream(size); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + out.close(); + + ContentValues values = new ContentValues(); + values.put(FIELD_APPWIDGET_ID, appWidgetId); + values.put(FIELD_WIDGET_TYPE, TYPE_SINGLE_PHOTO); + values.put(FIELD_IMAGE_URI, imageUri.toString()); + values.put(FIELD_PHOTO_BLOB, out.toByteArray()); + + SQLiteDatabase db = getWritableDatabase(); + db.replaceOrThrow(TABLE_WIDGETS, null, values); + return true; + } catch (Throwable e) { + Log.e(TAG, "set widget photo fail", e); + return false; + } + } + + public boolean setWidget(int id, int type, String albumPath) { + try { + ContentValues values = new ContentValues(); + values.put(FIELD_APPWIDGET_ID, id); + values.put(FIELD_WIDGET_TYPE, type); + values.put(FIELD_ALBUM_PATH, Utils.ensureNotNull(albumPath)); + getWritableDatabase().replaceOrThrow(TABLE_WIDGETS, null, values); + return true; + } catch (Throwable e) { + Log.e(TAG, "set widget fail", e); + return false; + } + } + + private static Bitmap loadBitmap(Cursor cursor, int columnIndex) { + byte[] data = cursor.getBlob(columnIndex); + if (data == null) return null; + return BitmapFactory.decodeByteArray(data, 0, data.length); + } + + public Entry getEntry(int appWidgetId) { + Cursor cursor = null; + try { + SQLiteDatabase db = getReadableDatabase(); + cursor = db.query(TABLE_WIDGETS, PROJECTION, + WHERE_CLAUSE, new String[] {String.valueOf(appWidgetId)}, + null, null, null); + if (cursor == null || !cursor.moveToNext()) { + Log.e(TAG, "query fail: empty cursor: " + cursor); + return null; + } + return new Entry(appWidgetId, cursor); + } catch (Throwable e) { + Log.e(TAG, "Could not load photo from database", e); + return null; + } finally { + Utils.closeSilently(cursor); + } + } + + /** + * Remove any bitmap associated with the given appWidgetId. + */ + public void deleteEntry(int appWidgetId) { + try { + SQLiteDatabase db = getWritableDatabase(); + db.delete(TABLE_WIDGETS, WHERE_CLAUSE, + new String[] {String.valueOf(appWidgetId)}); + } catch (SQLiteException e) { + Log.e(TAG, "Could not delete photo from database", e); + } + } +} \ No newline at end of file diff --git a/src/com/android/gallery3d/widget/WidgetProvider.java b/src/com/android/gallery3d/widget/WidgetProvider.java new file mode 100644 index 000000000..0a2fbfbe0 --- /dev/null +++ b/src/com/android/gallery3d/widget/WidgetProvider.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.widget; + +import com.android.gallery3d.R; +import com.android.gallery3d.widget.WidgetDatabaseHelper.Entry; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; +import android.widget.RemoteViews; + +public class WidgetProvider extends AppWidgetProvider { + + private static final String TAG = "WidgetProvider"; + + static RemoteViews buildWidget(Context context, int id, Entry entry) { + + switch (entry.type) { + case WidgetDatabaseHelper.TYPE_ALBUM: + case WidgetDatabaseHelper.TYPE_SHUFFLE: + return buildStackWidget(context, id, entry); + case WidgetDatabaseHelper.TYPE_SINGLE_PHOTO: + return buildFrameWidget(context, id, entry); + } + throw new RuntimeException("invalid type - " + entry.type); + } + + @Override + public void onUpdate(Context context, + AppWidgetManager appWidgetManager, int[] appWidgetIds) { + WidgetDatabaseHelper helper = new WidgetDatabaseHelper(context); + try { + for (int id : appWidgetIds) { + Entry entry = helper.getEntry(id); + if (entry != null) { + RemoteViews views = buildWidget(context, id, entry); + appWidgetManager.updateAppWidget(id, views); + } else { + Log.e(TAG, "cannot load widget: " + id); + } + } + } finally { + helper.close(); + } + super.onUpdate(context, appWidgetManager, appWidgetIds); + } + + private static RemoteViews buildStackWidget(Context context, int widgetId, Entry entry) { + RemoteViews views = new RemoteViews( + context.getPackageName(), R.layout.appwidget_main); + + Intent intent = new Intent(context, WidgetService.class); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId); + intent.putExtra(WidgetService.EXTRA_WIDGET_TYPE, entry.type); + intent.putExtra(WidgetService.EXTRA_ALBUM_PATH, entry.albumPath); + intent.setData(Uri.parse("widget://gallery/" + widgetId)); + + views.setRemoteAdapter(R.id.appwidget_stack_view, intent); + views.setEmptyView(R.id.appwidget_stack_view, R.id.appwidget_empty_view); + + Intent clickIntent = new Intent(context, WidgetClickHandler.class); + PendingIntent pendingIntent = PendingIntent.getActivity( + context, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT); + views.setPendingIntentTemplate(R.id.appwidget_stack_view, pendingIntent); + + return views; + } + + static RemoteViews buildFrameWidget(Context context, int appWidgetId, Entry entry) { + RemoteViews views = new RemoteViews( + context.getPackageName(), R.layout.photo_frame); + views.setImageViewBitmap(R.id.photo, entry.image); + Intent clickIntent = new Intent(context, + WidgetClickHandler.class).setData(entry.imageUri); + PendingIntent pendingClickIntent = PendingIntent.getActivity(context, 0, + clickIntent, PendingIntent.FLAG_CANCEL_CURRENT); + views.setOnClickPendingIntent(R.id.photo, pendingClickIntent); + return views; + } + + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + // Clean deleted photos out of our database + WidgetDatabaseHelper helper = new WidgetDatabaseHelper(context); + for (int appWidgetId : appWidgetIds) { + helper.deleteEntry(appWidgetId); + } + helper.close(); + } +} \ No newline at end of file diff --git a/src/com/android/gallery3d/widget/WidgetService.java b/src/com/android/gallery3d/widget/WidgetService.java new file mode 100644 index 000000000..aa167c768 --- /dev/null +++ b/src/com/android/gallery3d/widget/WidgetService.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.widget; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; + +import android.appwidget.AppWidgetManager; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +public class WidgetService extends RemoteViewsService { + + @SuppressWarnings("unused") + private static final String TAG = "GalleryAppWidgetService"; + + public static final String EXTRA_WIDGET_TYPE = "widget-type"; + public static final String EXTRA_ALBUM_PATH = "album-path"; + + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + int id = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + int type = intent.getIntExtra(EXTRA_WIDGET_TYPE, 0); + String albumPath = intent.getStringExtra(EXTRA_ALBUM_PATH); + + return new PhotoRVFactory((GalleryApp) getApplicationContext(), + id, type, albumPath); + } + + private static class EmptySource implements WidgetSource { + + @Override + public int size() { + return 0; + } + + @Override + public Bitmap getImage(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public Uri getContentUri(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public void setContentListener(ContentListener listener) {} + + @Override + public void reload() {} + + @Override + public void close() {} + } + + private static class PhotoRVFactory implements + RemoteViewsService.RemoteViewsFactory, ContentListener { + + private final int mAppWidgetId; + private final int mType; + private final String mAlbumPath; + private final GalleryApp mApp; + + private WidgetSource mSource; + + public PhotoRVFactory(GalleryApp app, int id, int type, String albumPath) { + mApp = app; + mAppWidgetId = id; + mType = type; + mAlbumPath = albumPath; + } + + @Override + public void onCreate() { + if (mType == WidgetDatabaseHelper.TYPE_ALBUM) { + Path path = Path.fromString(mAlbumPath); + DataManager manager = mApp.getDataManager(); + MediaSet mediaSet = (MediaSet) manager.getMediaObject(path); + mSource = mediaSet == null + ? new EmptySource() + : new MediaSetSource(mediaSet); + } else { + mSource = new LocalPhotoSource(mApp.getAndroidContext()); + } + mSource.setContentListener(this); + AppWidgetManager.getInstance(mApp.getAndroidContext()) + .notifyAppWidgetViewDataChanged( + mAppWidgetId, R.id.appwidget_stack_view); + } + + @Override + public void onDestroy() { + mSource.close(); + mSource = null; + } + + public int getCount() { + return mSource.size(); + } + + public long getItemId(int position) { + return position; + } + + public int getViewTypeCount() { + return 1; + } + + public boolean hasStableIds() { + return true; + } + + public RemoteViews getLoadingView() { + RemoteViews rv = new RemoteViews( + mApp.getAndroidContext().getPackageName(), + R.layout.appwidget_loading_item); + rv.setProgressBar(R.id.appwidget_loading_item, 0, 0, true); + return rv; + } + + public RemoteViews getViewAt(int position) { + Bitmap bitmap = mSource.getImage(position); + if (bitmap == null) return getLoadingView(); + RemoteViews views = new RemoteViews( + mApp.getAndroidContext().getPackageName(), + R.layout.appwidget_photo_item); + views.setImageViewBitmap(R.id.appwidget_photo_item, bitmap); + views.setOnClickFillInIntent(R.id.appwidget_photo_item, new Intent() + .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .setData(mSource.getContentUri(position))); + return views; + } + + @Override + public void onDataSetChanged() { + mSource.reload(); + } + + @Override + public void onContentDirty() { + AppWidgetManager.getInstance(mApp.getAndroidContext()) + .notifyAppWidgetViewDataChanged( + mAppWidgetId, R.id.appwidget_stack_view); + } + } +} diff --git a/src/com/android/gallery3d/widget/WidgetSource.java b/src/com/android/gallery3d/widget/WidgetSource.java new file mode 100644 index 000000000..3c73e882f --- /dev/null +++ b/src/com/android/gallery3d/widget/WidgetSource.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.widget; + +import com.android.gallery3d.data.ContentListener; + +import android.graphics.Bitmap; +import android.net.Uri; + +public interface WidgetSource { + public int size(); + public Bitmap getImage(int index); + public Uri getContentUri(int index); + public void setContentListener(ContentListener listener); + public void reload(); + public void close(); +} diff --git a/src/com/android/gallery3d/widget/WidgetTypeChooser.java b/src/com/android/gallery3d/widget/WidgetTypeChooser.java new file mode 100644 index 000000000..9718e0cb2 --- /dev/null +++ b/src/com/android/gallery3d/widget/WidgetTypeChooser.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.widget; + +import com.android.gallery3d.R; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.RadioGroup; +import android.widget.RadioGroup.OnCheckedChangeListener; + +public class WidgetTypeChooser extends Activity { + + private OnCheckedChangeListener mListener = new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + Intent data = new Intent() + .putExtra(WidgetConfigure.KEY_WIDGET_TYPE, checkedId); + setResult(RESULT_OK, data); + finish(); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTitle(R.string.widget_type); + setContentView(R.layout.choose_widget_type); + RadioGroup rg = (RadioGroup) findViewById(R.id.widget_type); + rg.setOnCheckedChangeListener(mListener); + + Button cancel = (Button) findViewById(R.id.cancel); + cancel.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }); + } +} diff --git a/src/com/android/gallery3d/widget/WidgetUtils.java b/src/com/android/gallery3d/widget/WidgetUtils.java new file mode 100644 index 000000000..481bbddbc --- /dev/null +++ b/src/com/android/gallery3d/widget/WidgetUtils.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.widget; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.util.ThreadPool; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Bitmap.Config; +import android.util.Log; + +public class WidgetUtils { + + private static final String TAG = "WidgetUtils"; + + private static int sStackPhotoWidth = 220; + private static int sStackPhotoHeight = 170; + + private WidgetUtils() { + } + + public static void initialize(Context context) { + Resources r = context.getResources(); + sStackPhotoWidth = r.getDimensionPixelSize(R.dimen.stack_photo_width); + sStackPhotoHeight = r.getDimensionPixelSize(R.dimen.stack_photo_height); + } + + public static Bitmap createWidgetBitmap(MediaItem image) { + Bitmap bitmap = image.requestImage(MediaItem.TYPE_THUMBNAIL) + .run(ThreadPool.JOB_CONTEXT_STUB); + if (bitmap == null) { + Log.w(TAG, "fail to get image of " + image.toString()); + return null; + } + return createWidgetBitmap(bitmap, image.getRotation()); + } + + public static Bitmap createWidgetBitmap(Bitmap bitmap, int rotation) { + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + + float scale; + if (((rotation / 90) & 1) == 0) { + scale = Math.max((float) sStackPhotoWidth / w, + (float) sStackPhotoHeight / h); + } else { + scale = Math.max((float) sStackPhotoWidth / h, + (float) sStackPhotoHeight / w); + } + + Bitmap target = Bitmap.createBitmap( + sStackPhotoWidth, sStackPhotoHeight, Config.ARGB_8888); + Canvas canvas = new Canvas(target); + canvas.translate(sStackPhotoWidth / 2, sStackPhotoHeight / 2); + canvas.rotate(rotation); + canvas.scale(scale, scale); + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG); + canvas.drawBitmap(bitmap, -w / 2, -h / 2, paint); + return target; + } +} -- cgit v1.2.3